| 14 | rodolico | 1 | #! /usr/bin/perl -w
 | 
        
           |  |  | 2 |   | 
        
           |  |  | 3 | #    archiveIMAP: moves old messages from one IMAP account to another
 | 
        
           |  |  | 4 | #    maintaining hierarchy.
 | 
        
           |  |  | 5 | #    see http://wiki.linuxservertech.com for additional information
 | 
        
           |  |  | 6 | #    Copyright (C) 2014  R. W. Rodolico
 | 
        
           |  |  | 7 | #
 | 
        
           |  |  | 8 | #    version 1.0, 20140818
 | 
        
           |  |  | 9 | #       Initial Release
 | 
        
           |  |  | 10 | #
 | 
        
           |  |  | 11 | #    version 1.0.1 20140819
 | 
        
           |  |  | 12 | #        Removed dependancy on Email::Simple
 | 
        
           |  |  | 13 | #        Allowed 'separator' as an element in either source or target
 | 
        
           |  |  | 14 | #
 | 
        
           | 50 | rodolico | 15 | #    version 2.0.0 20190817 RWR
 | 
        
           |  |  | 16 | #        Major revision.
 | 
        
           |  |  | 17 | #           Config is now YAML
 | 
        
           |  |  | 18 | #           Default section which will fill in the blanks for anything not filled in on an account, so creating a lot of accounts
 | 
        
           |  |  | 19 | #              with common values is easier to set up an maintain
 | 
        
           |  |  | 20 | #           Target folder is configurable on a per account basis, using tags <folder>, <year>, <month> (called hierachy)
 | 
        
           |  |  | 21 | #
 | 
        
           | 54 | rodolico | 22 | #    version 2.1.0 20190822 RWR
 | 
        
           |  |  | 23 | #           Added sleeptime parameter to target which makes process sleep a number of seconds between each mail transfer
 | 
        
           |  |  | 24 | #              We use HiRes, so this can be a decimal number (ie, 0.5 for half a second).
 | 
        
           |  |  | 25 | #
 | 
        
           |  |  | 26 | #
 | 
        
           | 14 | rodolico | 27 | #    This program is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 28 | #    it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 29 | #    the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 30 | #    (at your option) any later version.
 | 
        
           |  |  | 31 | #
 | 
        
           |  |  | 32 | #    This program is distributed in the hope that it will be useful,
 | 
        
           |  |  | 33 | #    but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 34 | #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 35 | #    GNU General Public License for more details.
 | 
        
           |  |  | 36 | #
 | 
        
           |  |  | 37 | #    You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 38 | #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           | 40 | rodolico | 39 | #
 | 
        
           |  |  | 40 | #    for required libraries
 | 
        
           | 42 | rodolico | 41 | #    apt-get -y install libnet-imap-simple-ssl-perl libyaml-tiny-perl libhash-merge-simple-perl libclone-perl libdate-manip-perl libemail-simple-perl
 | 
        
           | 14 | rodolico | 42 |   | 
        
           |  |  | 43 | use strict;
 | 
        
           |  |  | 44 | use warnings;
 | 
        
           |  |  | 45 | use Net::IMAP::Simple; # libnet-imap-simple-ssl-perl
 | 
        
           |  |  | 46 | use POSIX; # to get floor and ceil
 | 
        
           | 40 | rodolico | 47 | use YAML::Tiny; # apt-get libyaml-tiny-perl under debian
 | 
        
           |  |  | 48 | use Clone 'clone'; # libclone-perl
 | 
        
           |  |  | 49 | use Hash::Merge::Simple qw/ merge clone_merge /; # libhash-merge-simple-perl
 | 
        
           | 42 | rodolico | 50 | use Date::Manip; # libdate-manip-perl
 | 
        
           |  |  | 51 | use Email::Simple; # libemail-simple-perl
 | 
        
           |  |  | 52 | use Date::Parse;
 | 
        
           | 54 | rodolico | 53 | use Time::HiRes;
 | 
        
           | 14 | rodolico | 54 |   | 
        
           | 42 | rodolico | 55 |   | 
        
           | 40 | rodolico | 56 | use Data::Dumper;
 | 
        
           |  |  | 57 |   | 
        
           | 14 | rodolico | 58 | # globals
 | 
        
           | 40 | rodolico | 59 | my $CONFIG_FILE_NAME = 'archiveIMAP.yaml';
 | 
        
           | 14 | rodolico | 60 |   | 
        
           | 40 | rodolico | 61 | # the default values for everything. These are overridden
 | 
        
           |  |  | 62 | # by default values in the conf file (.yaml) or by individual
 | 
        
           |  |  | 63 | # accounts entries.
 | 
        
           |  |  | 64 | my $config = {
 | 
        
           |  |  | 65 |    'default' => {
 | 
        
           | 58 | rodolico | 66 |       # if set, most log stuff is sent to this file
 | 
        
           |  |  | 67 |       'logfile' => '',
 | 
        
           |  |  | 68 |       # defines the amount of crap which ends up in the logfile
 | 
        
           |  |  | 69 |       'loglevel' => 4,
 | 
        
           | 40 | rodolico | 70 |       # where the mail is going to
 | 
        
           |  |  | 71 |       'target' => {
 | 
        
           |  |  | 72 |                   # these have no defaults. They should be in the configuration file
 | 
        
           |  |  | 73 |                   # 'password' => 'password',
 | 
        
           |  |  | 74 |                   # 'username' => 'username',
 | 
        
           |  |  | 75 |                   # hierarchy can be any combination of <path>, <month> and <year>
 | 
        
           |  |  | 76 |                   'hierarchy' => '<path>',
 | 
        
           |  |  | 77 |                   # default target server
 | 
        
           |  |  | 78 |                   'server' => 'localhost',
 | 
        
           | 54 | rodolico | 79 |                   # amount of time to sleep between messages, in seconds (float)
 | 
        
           |  |  | 80 |                   'sleeptime' => 0.5,
 | 
        
           | 40 | rodolico | 81 |                  },
 | 
        
           |  |  | 82 |       # where the mail is coming from
 | 
        
           |  |  | 83 |       'source' => {
 | 
        
           |  |  | 84 |                   # these have no defaults. They should be in the configuration file
 | 
        
           |  |  | 85 |                   # 'password' => 'password',
 | 
        
           |  |  | 86 |                   # 'username' => 'username',
 | 
        
           |  |  | 87 |                   # Anything older than this is archived
 | 
        
           |  |  | 88 |                   # number of days unless followed by 'M' or 'Y', in which case it is
 | 
        
           |  |  | 89 |                   # multiplied by the number of days in a year (365.2425) or days in a month (30.5)
 | 
        
           |  |  | 90 |                   # may be a float, ie 1.25Y is the same as 15M
 | 
        
           |  |  | 91 |                   'age' => '1Y',
 | 
        
           |  |  | 92 |                   # if set to 1, any folders emptied out will be deleted EXCEPT system folders
 | 
        
           |  |  | 93 |                   'deleteEmptyFolders' => 0,
 | 
        
           |  |  | 94 |                   # default source server
 | 
        
           |  |  | 95 |                   'server' => 'localhost',
 | 
        
           |  |  | 96 |                   # these folders are considered system folders and never deleted, case insensitive
 | 
        
           |  |  | 97 |                   'system' => [
 | 
        
           |  |  | 98 |                                  'Outbox',
 | 
        
           |  |  | 99 |                                  'Sent Items',
 | 
        
           |  |  | 100 |                                  'INBOX'
 | 
        
           |  |  | 101 |                               ],
 | 
        
           |  |  | 102 |                   # these folders are ignored, ie not processed at all. Case insensitive
 | 
        
           |  |  | 103 |                   'ignore' => [
 | 
        
           |  |  | 104 |                                  'Deleted Messages',
 | 
        
           |  |  | 105 |                                  'Drafts',
 | 
        
           |  |  | 106 |                                  'Junk E-mail',
 | 
        
           |  |  | 107 |                                  'Junk',
 | 
        
           |  |  | 108 |                                  'Trash'
 | 
        
           |  |  | 109 |                                ],
 | 
        
           |  |  | 110 |                   # if 1, after successful copy to target, remove from source
 | 
        
           |  |  | 111 |                   'deleteOnSuccess' => 0
 | 
        
           |  |  | 112 |                },
 | 
        
           |  |  | 113 |       # if 1, does a dry run showing what would have happened
 | 
        
           |  |  | 114 |       'testing' => 0,
 | 
        
           |  |  | 115 |       # if 0, will not be processed
 | 
        
           |  |  | 116 |       'enabled' => 1,
 | 
        
           |  |  | 117 |    }
 | 
        
           | 42 | rodolico | 118 | };
 | 
        
           | 40 | rodolico | 119 |   | 
        
           | 58 | rodolico | 120 | # prints to a log file, if defined. Otherwise, prints to STDOUT
 | 
        
           |  |  | 121 | sub logit {
 | 
        
           |  |  | 122 |    my $priority = shift;
 | 
        
           |  |  | 123 |    return unless $priority <= $config->{'default'}->{'loglevel'};
 | 
        
           |  |  | 124 |    if ( $config->{'default'}->{'logfile'} ) {
 | 
        
           |  |  | 125 |       open( my $logfile, '>>', $config->{'default'}->{'logfile'} ) || die "Could not write to $config->{default}->{logfile}: $!\n";
 | 
        
           |  |  | 126 |       while ( my $message = shift ) {
 | 
        
           |  |  | 127 |          print $logfile "$message\n";
 | 
        
           |  |  | 128 |       }
 | 
        
           |  |  | 129 |       close( $logfile );
 | 
        
           |  |  | 130 |    } else {
 | 
        
           |  |  | 131 |       print STDOUT join( "\n", @_ ) . "\n";
 | 
        
           |  |  | 132 |    }
 | 
        
           |  |  | 133 |    return;
 | 
        
           |  |  | 134 | } # logit
 | 
        
           | 40 | rodolico | 135 |   | 
        
           | 58 | rodolico | 136 |   | 
        
           | 14 | rodolico | 137 | #
 | 
        
           |  |  | 138 | # find where the script is actually located as cfg should be there
 | 
        
           |  |  | 139 | #
 | 
        
           |  |  | 140 | sub getScriptLocation {
 | 
        
           |  |  | 141 |    use strict;
 | 
        
           |  |  | 142 |    use File::Spec::Functions qw(rel2abs);
 | 
        
           |  |  | 143 |    use File::Basename;
 | 
        
           |  |  | 144 |    return dirname(rel2abs($0));
 | 
        
           |  |  | 145 | }
 | 
        
           |  |  | 146 |   | 
        
           |  |  | 147 | #
 | 
        
           |  |  | 148 | # Read the configuration file from current location 
 | 
        
           |  |  | 149 | # and return it as a string
 | 
        
           |  |  | 150 | #
 | 
        
           |  |  | 151 | sub readConfig {
 | 
        
           |  |  | 152 |    my $scriptLocation = &getScriptLocation();
 | 
        
           |  |  | 153 |    if ( -e "$scriptLocation/$CONFIG_FILE_NAME" ) {
 | 
        
           | 40 | rodolico | 154 |       my $yaml = YAML::Tiny->read( "$scriptLocation/$CONFIG_FILE_NAME" );
 | 
        
           |  |  | 155 |       # use clone_merge to merge conf file into $config
 | 
        
           |  |  | 156 |       # overwrites anything in $config if it exists in the config file
 | 
        
           |  |  | 157 |       $config = clone_merge( $config, $yaml->[0] );
 | 
        
           |  |  | 158 |       return 1;
 | 
        
           | 14 | rodolico | 159 |    }
 | 
        
           | 40 | rodolico | 160 |    return 0;
 | 
        
           | 14 | rodolico | 161 | }
 | 
        
           |  |  | 162 |   | 
        
           | 40 | rodolico | 163 | # merges default into current account, overwriting anything not defined in account with
 | 
        
           |  |  | 164 | # value from default EXCEPT arrays labeled in @tags, which will be merged together.
 | 
        
           |  |  | 165 | sub fixupAccount {
 | 
        
           |  |  | 166 |    my ( $default, $account ) = @_;
 | 
        
           |  |  | 167 |   | 
        
           |  |  | 168 |    # these arrays, part of source, will be appended together instead of being overwritten
 | 
        
           |  |  | 169 |    my @tags = ( 'ignore', 'system' );
 | 
        
           |  |  | 170 |    # merge the tags in question. NOTE: they can only be in source
 | 
        
           |  |  | 171 |    foreach my $tag ( @tags) {
 | 
        
           |  |  | 172 |       if ( $default->{'source'}->{$tag} && $account->{'source'}->{$tag} ) {
 | 
        
           |  |  | 173 |          my @j = ( @{$default->{'source'}->{$tag}}, @{$account->{'source'}->{$tag}} );
 | 
        
           |  |  | 174 |          $account->{'source'}->{$tag} = \@j;
 | 
        
           |  |  | 175 |       }
 | 
        
           |  |  | 176 |    }
 | 
        
           |  |  | 177 |    # now, merge account and default, with account taking precedence.
 | 
        
           |  |  | 178 |    return  clone_merge(  $default, $account );
 | 
        
           |  |  | 179 |    #my $c = clone_merge(  $default, $account );
 | 
        
           |  |  | 180 |    #return $c;
 | 
        
           |  |  | 181 | }
 | 
        
           |  |  | 182 |   | 
        
           | 14 | rodolico | 183 | #
 | 
        
           |  |  | 184 | # Open an IMAP connection
 | 
        
           |  |  | 185 | #
 | 
        
           |  |  | 186 | sub openIMAPConnection {
 | 
        
           |  |  | 187 |    my ( $server, $username, $password ) = @_;
 | 
        
           |  |  | 188 |    my $imap = Net::IMAP::Simple->new( $server ) ||
 | 
        
           |  |  | 189 |     die "Unable to connect to IMAP: $Net::IMAP::Simple::errstr\n";
 | 
        
           |  |  | 190 |    # Log on
 | 
        
           |  |  | 191 |    if(!$imap->login( $username, $password )){
 | 
        
           |  |  | 192 |      die "Login failed: " . $imap->errstr . "\n";
 | 
        
           |  |  | 193 |    }
 | 
        
           |  |  | 194 |    return $imap;
 | 
        
           |  |  | 195 | }
 | 
        
           |  |  | 196 |   | 
        
           |  |  | 197 | #
 | 
        
           |  |  | 198 | # returns a string in proper format for RFC which is $age days ago
 | 
        
           | 40 | rodolico | 199 | # $age is a float, possibly followed by a single character modifier
 | 
        
           | 14 | rodolico | 200 | #
 | 
        
           |  |  | 201 | sub getDate {
 | 
        
           |  |  | 202 |    my $age = shift;
 | 
        
           | 40 | rodolico | 203 |    # allow modifier to age which contains 'Y' (years) or 'M' (months)
 | 
        
           |  |  | 204 |    # Simply set multiplier to the correct value, then multiply the value
 | 
        
           |  |  | 205 |    $age = lc( $age );
 | 
        
           |  |  | 206 |    if ( $age =~ m/([0-9.]+)([a-z])/ ) {
 | 
        
           | 42 | rodolico | 207 |       # ~0 is the maximum integer which can be stored. Shifting right one gives max unsigned integer
 | 
        
           |  |  | 208 |       my $multiplier = ($2 eq 'y' ? 365.2425 : ( $2 eq 'm' ? 30.5 : ~0 >> 1) );
 | 
        
           | 40 | rodolico | 209 |       $age = floor( $1 * $multiplier);
 | 
        
           |  |  | 210 |    }
 | 
        
           | 14 | rodolico | 211 |    my @months = ('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');
 | 
        
           |  |  | 212 |    my @now = localtime(time - 24 * 60 * 60 * $age);
 | 
        
           |  |  | 213 |    $now[4] = @months[$now[4]];
 | 
        
           |  |  | 214 |    $now[5] += 1900;
 | 
        
           |  |  | 215 |    my $date = sprintf( "%d-%s-%d", $now[3],$now[4],$now[5] ) ; # '1-Jan-2014';
 | 
        
           |  |  | 216 |    return $date;
 | 
        
           |  |  | 217 | }
 | 
        
           |  |  | 218 |   | 
        
           | 42 | rodolico | 219 |   | 
        
           |  |  | 220 | # calculateTargetFolder
 | 
        
           |  |  | 221 | # we are passed the target and the message
 | 
        
           |  |  | 222 | # pattern is carot (^) separated and may contain
 | 
        
           |  |  | 223 | # special placeholders <path>, <year>, <month>
 | 
        
           |  |  | 224 | # anything else is inserted directly
 | 
        
           |  |  | 225 | sub calculateTargetFolder {
 | 
        
           |  |  | 226 |    my ( $message, $source, $target ) = @_;
 | 
        
           |  |  | 227 |    # we may be sorting by date
 | 
        
           |  |  | 228 |    my $email = Email::Simple->new( join( '', @$message ) );
 | 
        
           |  |  | 229 |    my $msgDate = $email->header('Date');
 | 
        
           |  |  | 230 |    my @t = strptime( $msgDate );
 | 
        
           |  |  | 231 |    my $month = $t[4]+1;
 | 
        
           |  |  | 232 |    $month = '0' . $month if $month < 10;
 | 
        
           |  |  | 233 |    my $year = $t[5]+1900;
 | 
        
           | 50 | rodolico | 234 |    # also, may need the source hierarchy
 | 
        
           |  |  | 235 |    my $sourceFolder = join ( $target->{'separator'}, @{ $source->{'source folder list'} } );
 | 
        
           | 42 | rodolico | 236 |    # now, build the path on the new machine
 | 
        
           |  |  | 237 |    my $targetPattern = join( $target->{'separator'}, @{ $target->{'hierachy pattern'} } );
 | 
        
           |  |  | 238 |    $targetPattern =~ s/<path>/$sourceFolder/gi;
 | 
        
           |  |  | 239 |    $targetPattern =~ s/<month>/$month/gi;
 | 
        
           |  |  | 240 |    $targetPattern =~ s/<year>/$year/gi;
 | 
        
           |  |  | 241 |    # return the string we created, separated by
 | 
        
           |  |  | 242 |    # the delimiters for the target
 | 
        
           |  |  | 243 |    return $targetPattern;
 | 
        
           |  |  | 244 | }
 | 
        
           |  |  | 245 |   | 
        
           |  |  | 246 |   | 
        
           |  |  | 247 | # If folder has messages that match the criteria, move them to target
 | 
        
           |  |  | 248 | # creates the target folder if necessary
 | 
        
           | 14 | rodolico | 249 | #
 | 
        
           | 42 | rodolico | 250 | sub processFolder {
 | 
        
           |  |  | 251 |    my ( $source, $target, $folder, $TESTING ) = @_;
 | 
        
           | 58 | rodolico | 252 |    &logit( 1, '', "=== Processing $folder" );
 | 
        
           | 42 | rodolico | 253 |    my $sourceAccount = $source->{'connection'};
 | 
        
           |  |  | 254 |    my $targetAccount = $target->{'connection'};
 | 
        
           |  |  | 255 |    my $numMessages = 0;
 | 
        
           |  |  | 256 |    $sourceAccount->expunge_mailbox( $folder ); # clean it up so we don't copy deleted messages
 | 
        
           |  |  | 257 |    $sourceAccount->select( $folder ) or die "Could not connect to folder $folder\n"; # move into the correct folder for the source
 | 
        
           |  |  | 258 |    my @ids = $sourceAccount->search_sent_before( $source->{'before date'} ); # use sent_before to get the sent date from message
 | 
        
           |  |  | 259 |    return 0 unless @ids; # we have nothing to copy, so exit
 | 
        
           | 58 | rodolico | 260 |    &logit( 3, "Found " . scalar( @ids ) . " messages to process" );
 | 
        
           | 42 | rodolico | 261 |    # make life easier by precalculating some paths as array pointers
 | 
        
           |  |  | 262 |    my @sourceFolders = split( '\\' . $source->{'separator'}, $folder );
 | 
        
           |  |  | 263 |    my @pattern = split '\\^', $target->{'hierarchy'};
 | 
        
           |  |  | 264 |    $source->{'source folder list'} = \@sourceFolders;
 | 
        
           |  |  | 265 |    $target->{'hierachy pattern'} = \@pattern;
 | 
        
           |  |  | 266 |   | 
        
           |  |  | 267 |    # process each message to be done
 | 
        
           | 56 | rodolico | 268 |    while ( my $id = shift ( @ids ) ) {
 | 
        
           | 42 | rodolico | 269 |       # get the flags
 | 
        
           |  |  | 270 |       my @flags = $sourceAccount->msg_flags( $id );
 | 
        
           |  |  | 271 |       # get the message
 | 
        
           | 58 | rodolico | 272 |       my $message = $sourceAccount->get( $id ) or die "Error getting message ID $id: $sourceAccount->errstr\n";
 | 
        
           | 42 | rodolico | 273 |       # calculate where we are going to move this to
 | 
        
           |  |  | 274 |       my $targetFolder = &calculateTargetFolder( $message, $source, $target );
 | 
        
           |  |  | 275 |       if ( $TESTING ) {
 | 
        
           | 58 | rodolico | 276 |          &logit( 0, "Would have " . ( $source->{'deleteOnSuccess'} ? 'moved' : 'copied' )  . " message to $targetFolder" );
 | 
        
           | 42 | rodolico | 277 |          next;
 | 
        
           |  |  | 278 |       }
 | 
        
           | 50 | rodolico | 279 |       if ( $target->{'connection'}->select( $targetFolder ) || &makeFolder( $target->{'connection'}, $targetFolder, $target->{'separator'} ) ) {
 | 
        
           |  |  | 280 |          if ( $target->{'connection'}->put( $targetFolder, $message, @flags ) ) {
 | 
        
           | 42 | rodolico | 281 |             $source->{'connection'}->delete( $id ) if ( $source->{'deleteOnSuccess'} ) ;
 | 
        
           | 54 | rodolico | 282 |             Time::HiRes::sleep( $target->{'sleeptime'} ) if $target->{'sleeptime'};
 | 
        
           | 42 | rodolico | 283 |             $numMessages++;
 | 
        
           |  |  | 284 |          } else {
 | 
        
           | 50 | rodolico | 285 |             die "Could not write to target, aborting\n$targetFolder->{'connection'}->errstr\n";
 | 
        
           | 42 | rodolico | 286 |          }
 | 
        
           |  |  | 287 |       } else {
 | 
        
           | 50 | rodolico | 288 |          warn "\t\t$targetFolder not found in target and could not create it\n";
 | 
        
           | 42 | rodolico | 289 |       }
 | 
        
           |  |  | 290 |   | 
        
           |  |  | 291 |    }
 | 
        
           |  |  | 292 |    return $numMessages;
 | 
        
           |  |  | 293 | }
 | 
        
           |  |  | 294 |   | 
        
           |  |  | 295 |   | 
        
           |  |  | 296 |   | 
        
           |  |  | 297 | #
 | 
        
           | 14 | rodolico | 298 | # Get a list of all folders to be processed
 | 
        
           |  |  | 299 | # currently, it just weeds out items in the ignore list
 | 
        
           |  |  | 300 | #
 | 
        
           |  |  | 301 | sub getFolders {
 | 
        
           |  |  | 302 |    my ($imap, $ignore, $separator) = @_;
 | 
        
           |  |  | 303 |    $separator = '\\' . $separator;
 | 
        
           |  |  | 304 |    # build a regex that will be used to filter the input
 | 
        
           |  |  | 305 |    # assuming Trash, Drafts and Junk are in the ignore list
 | 
        
           |  |  | 306 |    # and a period is the separator, the generated regex is
 | 
        
           |  |  | 307 |    # (^|(\.))((Trash)|(Drafts)|(Junk))((\.)|$)
 | 
        
           |  |  | 308 |    # which basically says ignore those folders, but not substrings of them
 | 
        
           |  |  | 309 |    # ie, Junk02 would not be filtered but Junk would
 | 
        
           |  |  | 310 |    my $ignoreRegex = "(^|($separator))((" . join( ")\|(", @$ignore ) . "))(($separator)|\$)";
 | 
        
           |  |  | 311 |    # read all mailboxes and filter them with above regex into @boxes
 | 
        
           | 42 | rodolico | 312 |    my @boxes = grep{ ! /$ignoreRegex/i } $imap->mailboxes;
 | 
        
           | 14 | rodolico | 313 |    return \@boxes;
 | 
        
           |  |  | 314 | }
 | 
        
           |  |  | 315 |   | 
        
           |  |  | 316 | #
 | 
        
           |  |  | 317 | # make a folder on the IMAP account. The folder is assumed to be the
 | 
        
           |  |  | 318 | # fully qualified path with the correct delimiters
 | 
        
           |  |  | 319 | #
 | 
        
           |  |  | 320 | sub makeFolder {
 | 
        
           |  |  | 321 |    my ($imap, $folder, $delimiter) = @_;
 | 
        
           |  |  | 322 |   | 
        
           | 58 | rodolico | 323 |    &logit( 3,  "\n\t\tCreating folder $folder" );
 | 
        
           | 14 | rodolico | 324 |    # you must create the parent folder before creating the children
 | 
        
           |  |  | 325 |    my $escapedDelimiter = '\\' . $delimiter;
 | 
        
           |  |  | 326 |    my @folders = split( $escapedDelimiter, $folder );
 | 
        
           |  |  | 327 |    $folder = '';
 | 
        
           |  |  | 328 |    # take them from the left and, if they don't exist, create it
 | 
        
           |  |  | 329 |    while ( my $subdir = shift @folders ) {
 | 
        
           |  |  | 330 |       $folder .= $delimiter if $folder;
 | 
        
           |  |  | 331 |       $folder .= $subdir;
 | 
        
           |  |  | 332 |       next if $imap->select( $folder ); # already created, so look deeper in hierachy
 | 
        
           | 58 | rodolico | 333 |       &logit( 1, "\n\t\t\tCreating subfolder $folder" );
 | 
        
           | 14 | rodolico | 334 |       $imap->create_mailbox( $folder ) || warn $imap->errstr();
 | 
        
           |  |  | 335 |       $imap->folder_subscribe( $folder ) || die $imap->errstr();
 | 
        
           |  |  | 336 |       unless ( $imap->select( $folder ) ) { # verify it was created
 | 
        
           |  |  | 337 |          warn "Unable to create $folder on target account\n";
 | 
        
           |  |  | 338 |          return 0;
 | 
        
           |  |  | 339 |       } # unless
 | 
        
           |  |  | 340 |    } # while
 | 
        
           |  |  | 341 |    return $folder;
 | 
        
           |  |  | 342 | }
 | 
        
           |  |  | 343 |   | 
        
           |  |  | 344 | #
 | 
        
           |  |  | 345 | # Delete an IMAP folder
 | 
        
           |  |  | 346 | #
 | 
        
           |  |  | 347 | sub deleteAFolder {
 | 
        
           | 42 | rodolico | 348 |    my ($source, $folder, $TESTING ) = @_;
 | 
        
           |  |  | 349 |    my $sourceAccount = $source->{'connection'};
 | 
        
           |  |  | 350 |    my $separator = $source->{'separator'};
 | 
        
           | 14 | rodolico | 351 |    return 1 if $folder eq 'INBOX'; # do NOT mess with INBOX
 | 
        
           |  |  | 352 |    return 2 if $sourceAccount->select($folder) > 0; # do not mess with it if it still has messages in it
 | 
        
           |  |  | 353 |    return 3 if $sourceAccount->mailboxes( $folder . $separator . '*' ); # do not mess with it if it has subfolders
 | 
        
           | 42 | rodolico | 354 |    return 4 if $source->{'system folders'}->{lc $folder}; # do not mess with system folders
 | 
        
           | 58 | rodolico | 355 |    &logit( 1, "\n\t\tDeleting empty folder $folder" . ( $TESTING ? ' Dry Run' : '' ) );
 | 
        
           |  |  | 356 |    return 0 if $TESTING;
 | 
        
           |  |  | 357 |    # select something other than the folder to be deleted
 | 
        
           |  |  | 358 |    $sourceAccount->select( 'INBOX' );
 | 
        
           |  |  | 359 |    if ( $sourceAccount->folder_unsubscribe($folder) ) {
 | 
        
           |  |  | 360 |       if ( $sourceAccount->delete_mailbox( $folder ) ) {
 | 
        
           |  |  | 361 |          return 0;
 | 
        
           |  |  | 362 |       } else {
 | 
        
           |  |  | 363 |          warn "Error trying to delete mailbox $folder: " . $sourceAccount->errstr . "\n";
 | 
        
           |  |  | 364 |       }
 | 
        
           |  |  | 365 |    } else {
 | 
        
           |  |  | 366 |       warn "Error trying to unsubscribe from $folder: " . $sourceAccount->errstr . "\n";
 | 
        
           |  |  | 367 |    }
 | 
        
           |  |  | 368 |    return 0;
 | 
        
           | 14 | rodolico | 369 | }
 | 
        
           |  |  | 370 |   | 
        
           |  |  | 371 |   | 
        
           | 40 | rodolico | 372 | # main process loop to handle one account
 | 
        
           | 14 | rodolico | 373 | #
 | 
        
           |  |  | 374 | sub processAccount {
 | 
        
           | 40 | rodolico | 375 |    my $account = shift;
 | 
        
           |  |  | 376 |   | 
        
           | 53 | rodolico | 377 |    return 0 unless $account->{'enabled'}; # blow it off if it is not enabled
 | 
        
           | 42 | rodolico | 378 |    my $TESTING = $account->{'testing'}; # create mini global if we should test this account
 | 
        
           | 50 | rodolico | 379 |   | 
        
           | 58 | rodolico | 380 |    &logit( 0, "========= Test Mode ========\n" ) if $TESTING;
 | 
        
           | 14 | rodolico | 381 |   | 
        
           |  |  | 382 |    # open and log into both source and target, and get the separator used
 | 
        
           | 42 | rodolico | 383 |    foreach my $acct ( 'target','source' ) {
 | 
        
           |  |  | 384 |       $account->{$acct}->{'connection'} = &openIMAPConnection( $account->{$acct}->{'server'}, $account->{$acct}->{'username'}, $account->{$acct}->{'password'} );
 | 
        
           |  |  | 385 |       unless ( $account->{$acct}->{'connection'} ) {
 | 
        
           |  |  | 386 |          warn "Unable to open $acct for $account->{$acct}->{username}, aborting move: $!\n";
 | 
        
           |  |  | 387 |          return -1;
 | 
        
           |  |  | 388 |       }
 | 
        
           |  |  | 389 |       $account->{$acct}->{'separator'} = $account->{$acct}->{'connection'}->separator unless $account->{$acct}->{'separator'};
 | 
        
           |  |  | 390 |    }
 | 
        
           | 14 | rodolico | 391 |   | 
        
           | 42 | rodolico | 392 |    # just being set up for convenience and readability
 | 
        
           |  |  | 393 |    my $source = $account->{'source'};
 | 
        
           |  |  | 394 |    my $target = $account->{'target'};
 | 
        
           |  |  | 395 |   | 
        
           |  |  | 396 |    my %temp = map{ lc($_) => 1 } @{$source->{'system'}};
 | 
        
           |  |  | 397 |    $source->{'system folders'} = \%temp;
 | 
        
           |  |  | 398 |   | 
        
           | 50 | rodolico | 399 |    $source->{'before date'} = &getDate( $source->{'age'} );
 | 
        
           | 58 | rodolico | 400 |    &logit( 1, "\t" . ( $source->{'deleteOnSuccess'} ? 'Moving' : 'Copying' ) . " all messages before $source->{'before date'}" );
 | 
        
           | 42 | rodolico | 401 |   | 
        
           | 14 | rodolico | 402 |    # get a list of all folders to be processed on the source
 | 
        
           | 42 | rodolico | 403 |    $source->{'folders'} = &getFolders( $source->{'connection'}, $source->{'ignore'}, $source->{'separator'} );
 | 
        
           |  |  | 404 |   | 
        
           | 50 | rodolico | 405 |    if ( $TESTING ) {
 | 
        
           |  |  | 406 |       print Dumper( $source );
 | 
        
           |  |  | 407 |       print "Source above, press enter to continue: "; my $j = <STDIN>;
 | 
        
           |  |  | 408 |       print Dumper( $target );
 | 
        
           |  |  | 409 |       print "Target above, Press enter to continue: "; $j = <STDIN>;
 | 
        
           |  |  | 410 |    }
 | 
        
           | 42 | rodolico | 411 |   | 
        
           |  |  | 412 |    my $folderList = $source->{'folders'};
 | 
        
           | 14 | rodolico | 413 |    my $count = 0; # count the number of messages processed
 | 
        
           |  |  | 414 |    my $processedCount = 0; # count the number of folders processed
 | 
        
           |  |  | 415 |    foreach my $folder ( @$folderList ) {
 | 
        
           | 38 | rodolico | 416 |       my $messages;
 | 
        
           | 42 | rodolico | 417 |       $messages = &processFolder( $source, $target, $folder, $TESTING ); #, $date, $$source{'separator'}, $$target{'separator'}, $deleteOnSuccess );
 | 
        
           |  |  | 418 |   | 
        
           |  |  | 419 |       $TESTING ? print "Would expunge $folder\n" : $source->{'connection'}->expunge_mailbox( $folder );
 | 
        
           | 14 | rodolico | 420 |       # delete folder if empty and client has requested it.
 | 
        
           | 58 | rodolico | 421 |       if ( $account->{'source'}->{'deleteEmptyFolders'} ) {
 | 
        
           |  |  | 422 |          my $returnCode = &deleteAFolder( $source, $folder, $TESTING );
 | 
        
           |  |  | 423 |          &logit( 3, "Return code from deleteAFolder is $returnCode" );
 | 
        
           |  |  | 424 |       }
 | 
        
           |  |  | 425 |       &logit( 1,  "$messages processed" ); 
 | 
        
           | 14 | rodolico | 426 |       $count += $messages;
 | 
        
           |  |  | 427 |       $processedCount++;
 | 
        
           |  |  | 428 |       # next line used only for testing. Dies after 5 folders on first account
 | 
        
           | 50 | rodolico | 429 |       last if $processedCount > 5 and $TESTING;
 | 
        
           | 14 | rodolico | 430 |    }
 | 
        
           | 42 | rodolico | 431 |   | 
        
           |  |  | 432 |    $source->{'connection'}->quit;
 | 
        
           |  |  | 433 |    $target->{'connection'}->quit;
 | 
        
           | 14 | rodolico | 434 |    return $count;
 | 
        
           |  |  | 435 | }
 | 
        
           |  |  | 436 |   | 
        
           | 40 | rodolico | 437 |   | 
        
           |  |  | 438 |   | 
        
           | 14 | rodolico | 439 | #######################################################################
 | 
        
           |  |  | 440 | #                   Main                                              #
 | 
        
           |  |  | 441 | #######################################################################
 | 
        
           |  |  | 442 |   | 
        
           |  |  | 443 | # read and evaluate configuration file
 | 
        
           | 40 | rodolico | 444 | &readConfig() || die "could not load config file\n";
 | 
        
           | 58 | rodolico | 445 |   | 
        
           | 40 | rodolico | 446 | #print Dumper( $config ); die;
 | 
        
           |  |  | 447 | foreach my $account ( keys %{$config->{'accounts'}} ) {
 | 
        
           |  |  | 448 |     $config->{'accounts'}->{$account} = &fixupAccount( $config->{'default'}, $config->{'accounts'}->{$account} );
 | 
        
           | 14 | rodolico | 449 | }
 | 
        
           |  |  | 450 |   | 
        
           | 40 | rodolico | 451 | #print Dumper( $config ) ; die;
 | 
        
           | 14 | rodolico | 452 |   | 
        
           | 40 | rodolico | 453 | # just a place to gather some stats
 | 
        
           | 14 | rodolico | 454 | my %processed;
 | 
        
           |  |  | 455 | $processed{'Accounts'} = 0;
 | 
        
           |  |  | 456 | $processed{'Messages'} = 0;
 | 
        
           |  |  | 457 |   | 
        
           | 40 | rodolico | 458 | # grab only the accounts for simplicity
 | 
        
           |  |  | 459 | my $accounts = $config->{'accounts'};
 | 
        
           | 42 | rodolico | 460 |   | 
        
           | 50 | rodolico | 461 | #die Dumper( $accounts );
 | 
        
           | 42 | rodolico | 462 |   | 
        
           | 40 | rodolico | 463 | # now, process each in turn
 | 
        
           |  |  | 464 | foreach my $account ( keys %$accounts ) {
 | 
        
           | 14 | rodolico | 465 |    # talk to user
 | 
        
           | 58 | rodolico | 466 |    &logit( 1,  "Processing account $account" );
 | 
        
           | 40 | rodolico | 467 |    # do the account. This is the main worker bee
 | 
        
           |  |  | 468 |    $accounts->{$account}->{'processed'} = &processAccount( $accounts->{$account} );
 | 
        
           | 58 | rodolico | 469 |    &logit( 1,  "Done, $accounts->{$account}->{'processed'} messages copied" );
 | 
        
           | 14 | rodolico | 470 |    $processed{'Accounts'}++;
 | 
        
           | 40 | rodolico | 471 |    $processed{'Messages'} += $accounts->{$account}->{'processed'};
 | 
        
           | 42 | rodolico | 472 |    # free up space we allocated since we stored a bunch of stuff in there, and we don't need it anymore
 | 
        
           |  |  | 473 |    $accounts->{$account} = undef; 
 | 
        
           | 14 | rodolico | 474 | } # foreach loop
 | 
        
           |  |  | 475 |   | 
        
           | 58 | rodolico | 476 | &logit( 1, "$processed{Accounts} accounts processed, $processed{Messages} messages" );
 | 
        
           | 14 | rodolico | 477 |   | 
        
           | 40 | rodolico | 478 |   | 
        
           | 14 | rodolico | 479 | 1;
 | 
        
           |  |  | 480 |   |