1#!/usr/local/bin/perl
2
3# $Header: /mhub4/sources/imap-tools/mbxIMAPsync.pl,v 1.1 2008/10/18 15:09:25 rick Exp $
4
5use Socket;
6use FileHandle;
7use Fcntl;
8use Getopt::Std;
9
10    ######################################################################
11    #  Program name   mbxIMAPsync.pl                                     #
12    #  Written by     Rick Sanders                                       #
13    #  Date           12 Feb 2004                                        #
14    #                                                                    #
15    #  Description                                                       #
16    #                                                                    #
17    #  mbxIMAPsync is used to synchronize the contents of a Unix         #
18    #  mailfiles with an IMAP mailbox.  The user supplies the location   #
19    #  & name of the Unix mailbox (eg /var/mail/rfs) and the hostname,   #
20    #  username, & password of the IMAP account along with the name      #
21    #  of the IMAP mailbox.  For example:                                #
22    #                                                                    #
23    #  ./mbxIMAPsync.pl -f /var/mail/rfs -i imapsrv/rfs/mypass -m INBOX  #
24    #                                                                    #
25    #  mbxIMAPsync compares the messages in the mailfile with those in   #
26    #  the IMAP mailbox by Message-Id and adds the ones in the mailfile  #
27    #  which are not in the IMAP mailbox.  Then it looks for messages    #
28    #  in the IMAP mailbox which are not in the mailfile and removes     #
29    #  them from the IMAP mailbox.                                       #
30    #                                                                    #
31    #  See the Usage() for available options.                            #
32    ######################################################################
33
34    &init();
35
36   &connectToHost($imapHost, 'IMAP');
37   &login($imapUser,$imapPwd, 'IMAP');
38
39   #  Get list of msgs in the mailfile by Message-Id
40
41   $added=$purged=0;
42   print STDOUT "Processing $mailfile\n";
43   print STDOUT "Checking for messages to add\n";
44   @msgs = &readMbox( $mailfile );
45   foreach $msg ( @msgs ) {
46       @msgid = grep( /^Message-ID:/i, @$msg );
47       ($label,$msgid) = split(/:/, $msgid[0]);
48       chomp $msgid;
49       &trim( *msgid );
50       $mailfileMsgs{"$msgid"} = '1';
51       push( @sourceMsgs, $msgid );
52
53       if ( !&findMsg( $msgid, $mbx, 'IMAP' ) ) {
54          # print STDOUT "Need to add msgid >$msgid<\n";
55          my $message;
56
57          foreach $_ ( @$msg ) { chop $_; $message .= "$_\r\n"; }
58
59          if ( &insertMsg($mbx, \$message, $flags, $date, 'IMAP') ) {
60             $added++;
61             print STDOUT "   Added $msgid\n";
62          }
63       }
64   }
65
66   #  Remove any messages from the IMAP mailbox that no longer
67   #  exist in the mailfile
68
69   print STDOUT "Checking for messages to purge\n";
70   &getMsgList( $mbx, \@imapMsgs, 'IMAP' );
71   foreach $msgid ( @imapMsgs ) {
72      if ( $mailfileMsgs{"$msgid"} eq '' ) {
73         if ( &deleteMsg($msgid, $mbx, 'IMAP') ) {
74            &Log("   Marked $msgid for deletion");
75            print STDOUT "   Marked msgid $msgid for deletion\n";
76            $deleted++;
77         }
78      }
79   }
80
81   if ( $deleted ) {
82      #  Need to purge the deleted messages
83      $purged = &expungeMbx( $mbx, 'IMAP' );
84   }
85
86   &Log("Done");
87   &Log("Added  $added messages to IMAP mailbox $mbx");
88   &Log("Purged $purged messages from IMAP mailbox $mbx");
89
90   print STDOUT "\nAdded  $added messages to IMAP mailbox $mbx\n";
91   print STDOUT "Purged $purged messages from IMAP mailbox $mbx\n";
92
93   exit;
94
95
96sub init {
97
98   if ( ! getopts('f:m:i:L:dx') ) {
99      &usage();
100      exit;
101   }
102
103   ($imapHost,$imapUser,$imapPwd) = split(/\//, $opt_i);
104   $mailfile = $opt_f;
105   $mbx      = $opt_m;
106   $logfile  = $opt_L;
107   $debug    = 1 if $opt_d;
108
109   if ( $logfile ) {
110      if ( ! open (LOG, ">> $logfile") ) {
111        print "Can't open logfile $logfile: $!\n";
112        $logfile = '';
113      }
114   }
115   Log("\nThis is mbxIMAPsync\n");
116
117   if ( !-e $mailfile ) {
118      &Log("$mailfile does not exist");
119      exit;
120   }
121
122}
123
124sub usage {
125
126   print "Usage: mbxIMAPsync.pl\n";
127   print "    -f <location of mailfiles>\n";
128   print "    -i imapHost/imapUser/imapPassword\n";
129   print "    -m <IMAP mailbox>\n";
130   print "    [-L <logfile>]\n";
131   print "    [-d debug]\n";
132
133}
134
135sub readMbox {
136
137my $file  = shift;
138my @mail  = ();
139my $mail  = [];
140my $blank = 1;
141local *FH;
142local $_;
143
144    &Log("Reading the mailfile") if $debug;
145    open(FH,"< $file") or die "Can't open $file";
146
147    while(<FH>) {
148        if($blank && /\AFrom .*\d{4}/) {
149            push(@mail, $mail) if scalar(@{$mail});
150            $mail = [ $_ ];
151            $blank = 0;
152        }
153        else {
154            $blank = m#\A\Z#o ? 1 : 0;
155            push(@{$mail}, $_);
156        }
157    }
158
159    push(@mail, $mail) if scalar(@{$mail});
160    close(FH);
161
162    return wantarray ? @mail : \@mail;
163}
164
165sub Log {
166
167my $line = shift;
168my $msg;
169
170   ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime (time);
171   $msg = sprintf ("%.2d-%.2d-%.4d.%.2d:%.2d:%.2d %s",
172                  $mon + 1, $mday, $year + 1900, $hour, $min, $sec, $line);
173
174   if ( $logfile ) {
175      print LOG "$msg\n";
176   } else {
177      print "$line\n";
178   }
179
180}
181
182#  connectToHost
183#
184#  Make an IMAP4 connection to a host
185#
186sub connectToHost {
187
188my $host = shift;
189my $conn = shift;
190
191   &Log("Connecting to $host") if $debug;
192
193   $sockaddr = 'S n a4 x8';
194   ($name, $aliases, $proto) = getprotobyname('tcp');
195   $port = 143;
196
197   if ($host eq "") {
198	&Log ("no remote host defined");
199	close LOG;
200	exit (1);
201   }
202
203   ($name, $aliases, $type, $len, $serverAddr) = gethostbyname ($host);
204   if (!$serverAddr) {
205	&Log ("$host: unknown host");
206	close LOG;
207	exit (1);
208   }
209
210   #  Connect to the IMAP4 server
211   #
212
213   $server = pack ($sockaddr, &AF_INET, $port, $serverAddr);
214   if (! socket($conn, &PF_INET, &SOCK_STREAM, $proto) ) {
215	&Log ("socket: $!");
216	close LOG;
217	exit (1);
218   }
219   if ( ! connect( $conn, $server ) ) {
220	&Log ("connect: $!");
221	return 0;
222   }
223
224   select( $conn ); $| = 1;
225   while (1) {
226	&readResponse ( $conn );
227	if ( $response =~ /^\* OK/i ) {
228	   last;
229	}
230	else {
231 	   &Log ("Can't connect to host on port $port: $response");
232	   return 0;
233	}
234   }
235   &Log ("connected to $host") if $debug;
236
237   select( $conn ); $| = 1;
238   return 1;
239}
240
241#
242#  login in at the source host with the user's name and password
243#
244sub login {
245
246my $user = shift;
247my $pwd  = shift;
248my $conn = shift;
249
250   &Log("Logging in as $user") if $debug;
251   $rsn = 1;
252   &sendCommand ($conn, "$rsn LOGIN $user $pwd");
253   while (1) {
254	&readResponse ( $conn );
255	if ($response =~ /^$rsn OK/i) {
256		last;
257	}
258	elsif ($response =~ /NO/) {
259		&Log ("unexpected LOGIN response: $response");
260		return 0;
261	}
262   }
263   &Log("Logged in as $user") if $debug;
264
265   return 1;
266}
267
268
269#  logout
270#
271#  log out from the host
272#
273sub logout {
274
275my $conn = shift;
276
277   ++$lsn;
278   undef @response;
279   &sendCommand ($conn, "$lsn LOGOUT");
280   while ( 1 ) {
281	&readResponse ($conn);
282	if ( $response =~ /^$lsn OK/i ) {
283		last;
284	}
285	elsif ( $response !~ /^\*/ ) {
286		&Log ("unexpected LOGOUT response: $response");
287		last;
288	}
289   }
290   close $conn;
291   return;
292}
293
294#  readResponse
295#
296#  This subroutine reads and formats an IMAP protocol response from an
297#  IMAP server on a specified connection.
298#
299
300sub readResponse
301{
302    local($fd) = shift @_;
303
304    $response = <$fd>;
305    chop $response;
306    $response =~ s/\r//g;
307    push (@response,$response);
308    if ($debug) { &Log ("<< $response",2); }
309}
310
311#
312#  sendCommand
313#
314#  This subroutine formats and sends an IMAP protocol command to an
315#  IMAP server on a specified connection.
316#
317
318sub sendCommand
319{
320    local($fd) = shift @_;
321    local($cmd) = shift @_;
322
323    print $fd "$cmd\r\n";
324
325    if ($showIMAP) { &Log (">> $cmd",2); }
326}
327
328#
329sub insertMsg {
330
331my $mbx = shift;
332my $message = shift;
333my $flags = shift;
334my $date  = shift;
335my $conn  = shift;
336my ($lsn,$lenx);
337
338   &Log("   Inserting message into mailbox $mbx") if $debug;
339   $lenx = length($$message);
340
341   #  Create the mailbox unless we have already done so
342   ++$lsn;
343   if ($destMbxs{"$mbx"} eq '') {
344        &Log("creating mailbox $mbx") if $debug;
345	&sendCommand (IMAP, "$lsn CREATE \"$mbx\"");
346	while ( 1 ) {
347	   &readResponse (IMAP);
348	   if ( $response =~ /^$rsn OK/i ) {
349		last;
350	   }
351	   elsif ( $response !~ /^\*/ ) {
352		if (!($response =~ /already exists|reserved mailbox name/i)) {
353			&Log ("WARNING: $response");
354		}
355		last;
356	   }
357       }
358   }
359
360   $destMbxs{"$mbx"} = '1';
361
362   ++$lsn;
363   $flags =~ s/\\Recent//i;
364
365   # &sendCommand (IMAP, "$lsn APPEND \"$mbx\" ($flags) \"$date\" \{$lenx\}");
366   &sendCommand (IMAP, "$lsn APPEND \"$mbx\" \{$lenx\}");
367   &readResponse (IMAP);
368   if ( $response !~ /^\+/ ) {
369       &Log ("unexpected APPEND response: $response");
370       # next;
371       push(@errors,"Error appending message to $mbx for $user");
372       return 0;
373   }
374
375   print IMAP "$$message\r\n";
376
377   undef @response;
378   while ( 1 ) {
379       &readResponse (IMAP);
380       if ( $response =~ /^$lsn OK/i ) {
381	   last;
382       }
383       elsif ( $response !~ /^\*/ ) {
384	   &Log ("unexpected APPEND response: $response");
385	   # next;
386	   return 0;
387       }
388   }
389
390   return 1;
391}
392
393#  getMsgList
394#
395#  Get a list of the user's messages in the indicated mailbox on
396#  the IMAP host
397#
398sub getMsgList {
399
400my $mailbox = shift;
401my $msgs    = shift;
402my $conn    = shift;
403my $seen;
404my $empty;
405my $msgnum;
406
407   &Log("Getting list of msgs in $mailbox") if $debug;
408   &trim( *mailbox );
409   &sendCommand ($conn, "$rsn EXAMINE \"$mailbox\"");
410   undef @response;
411   $empty=0;
412   while ( 1 ) {
413	&readResponse ( $conn );
414	if ( $response =~ / 0 EXISTS/i ) { $empty=1; }
415	if ( $response =~ /^$rsn OK/i ) {
416		# print STDERR "response $response\n";
417		last;
418	}
419	elsif ( $response !~ /^\*/ ) {
420		&Log ("unexpected response: $response");
421		# print STDERR "Error: $response\n";
422		return 0;
423	}
424   }
425
426   &sendCommand ( $conn, "$rsn FETCH 1:* (uid flags internaldate body[header.fields (Message-Id)])");
427   undef @response;
428   while ( 1 ) {
429	&readResponse ( $conn );
430	if ( $response =~ /^$rsn OK/i ) {
431		# print STDERR "response $response\n";
432		last;
433	}
434	elsif ( $XDXDXD ) {
435		&Log ("unexpected response: $response");
436		&Log ("Unable to get list of messages in this mailbox");
437		push(@errors,"Error getting list of $user's msgs");
438		return 0;
439	}
440   }
441
442   #  Get a list of the msgs in the mailbox
443   #
444   undef @msgs;
445   undef $flags;
446   for $i (0 .. $#response) {
447	$seen=0;
448	$_ = $response[$i];
449
450	last if /OK FETCH complete/;
451
452	if ( $response[$i] =~ /FETCH \(UID / ) {
453	   $response[$i] =~ /\* ([^FETCH \(UID]*)/;
454	   $msgnum = $1;
455	}
456
457	if ($response[$i] =~ /FLAGS/) {
458	    #  Get the list of flags
459	    $response[$i] =~ /FLAGS \(([^\)]*)/;
460	    $flags = $1;
461   	    $flags =~ s/\\Recent//i;
462	}
463        if ( $response[$i] =~ /INTERNALDATE ([^\)]*)/ ) {
464	    ### $response[$i] =~ /INTERNALDATE (.+) ([^BODY]*)/i;
465	    $response[$i] =~ /INTERNALDATE (.+) BODY/i;
466            $date = $1;
467            $date =~ s/"//g;
468	}
469	if ( $response[$i] =~ /^Message-Id:/i ) {
470	    ($label,$msgid) = split(/: /, $response[$i]);
471	    push (@$msgs,$msgid);
472	}
473   }
474}
475
476#  trim
477#
478#  remove leading and trailing spaces from a string
479sub trim {
480
481local (*string) = @_;
482
483   $string =~ s/^\s+//;
484   $string =~ s/\s+$//;
485
486   return;
487}
488
489
490sub findMsg {
491
492my $msgid = shift;
493my $mbx   = shift;
494my $conn  = shift;
495my $msgnum;
496my $noSuchMbx;
497
498   &Log("Searching for $msgid in $mbx") if $debug;
499   &sendCommand ( $conn, "1 SELECT \"$mbx\"");
500   while (1) {
501	&readResponse ($conn);
502        if ( $response =~ /^1 NO/ ) {
503           $noSuchMbx = 1;
504           last;
505        }
506	last if $response =~ /^1 OK/;
507   }
508   return '' if $noSuchMbx;
509
510   &Log("Search for $msgid") if $debug;
511   &sendCommand ( $conn, "$rsn SEARCH header Message-Id \"$msgid\"");
512   while (1) {
513	&readResponse ($conn);
514	if ( $response =~ /\* SEARCH /i ) {
515	   ($dmy, $msgnum) = split(/\* SEARCH /i, $response);
516	   ($msgnum) = split(/ /, $msgnum);
517	}
518
519	last if $response =~ /^1 OK/;
520	last if $response =~ /complete/i;
521   }
522
523   if ( $msgnum ) {
524      &Log("Message exists") if $debug;
525   } else {
526      &Log("Message does not exist") if $debug;
527   }
528
529   return $msgnum;
530}
531
532sub deleteMsg {
533
534my $msgid = shift;
535my $mbx   = shift;
536my $conn  = shift;
537my $rc;
538
539   &Log("Deleting message $msgid") if $debug;
540   $msgnum = &findMsg( $msgid, $mbx, $conn );
541
542   &sendCommand ( $conn, "1 STORE $msgnum +FLAGS (\\Deleted)");
543   while (1) {
544        &readResponse ($conn);
545        if ( $response =~ /^1 OK/i ) {
546	   $rc = 1;
547	   &Log("   Marked $msgid for delete");
548	   last;
549	}
550
551	if ( $response =~ /^1 BAD|^1 NO/i ) {
552	   &Log("Error setting \Deleted flag for msg $msgnum: $response");
553	   $rc = 0;
554	   last;
555	}
556   }
557
558   return $rc;
559
560}
561
562sub expungeMbx {
563
564my $mbx   = shift;
565my $conn  = shift;
566my $purged=0;
567
568   &Log("Purging $mbx") if $debug;
569   &sendCommand ( $conn, "1 SELECT \"$mbx\"");
570   while (1) {
571        &readResponse ($conn);
572        last if $response =~ /^1 OK/;
573
574	if ( $response =~ /^1 NO|^1 BAD/i ) {
575	   &Log("Error selecting mailbox $mbx: $response");
576	   last;
577	}
578   }
579
580   &sendCommand ( $conn, "1 EXPUNGE");
581   while (1) {
582        &readResponse ($conn);
583        last if $response =~ /^1 OK/;
584        $purged++ if $response =~ /EXPUNGE/i;
585
586	if ( $response =~ /^1 BAD|^1 NO/i ) {
587	   print STDOUT "Error expunging messages: $response\n";
588	   last;
589	}
590   }
591
592   return $purged;
593
594}
595
596