#!/opt/local/bin/perl -w
# @(#) BIPserver.pl	Acquires Brother-Internet-Print jobs from a POP3 server
#			and passes them to designated printer(s).
#			Rev'd: 2002-12-18.
#
# Copyright (c) 2002 Graham Jenkins <grahjenk@au1.ibm.com>. All rights reserved.
# This program is free software; you can redistribute it and/or modify it under
# the same terms as Perl itself.

use strict;
use File::Basename;				# For security, you may wish to
use Net::POP3;					# authenticate each job-request
use Net::SMTP;					# by using a one-time or other
use Sys::Syslog;				# code in the REPLY field as a
use MIME::Base64;				# password to look-up a real
use Printer;					# REPLY address.
use Env qw(MAXMESS MAXJOB EXPIRE SLEEP);	# Maximum message-size (bytes),
use vars qw($VERSION);				# job-size (bytes), expire-time
$VERSION = "1.03";				# (secs) and sleep-time (secs).
my $maxmess=$MAXMESS?$MAXMESS:23000000;
my $maxjob =$MAXJOB ?$MAXJOB :33554432;
my $expire =$EXPIRE ?$EXPIRE :10800;
my $sleep  =$SLEEP  ?$SLEEP  :30;
my %uidtime;
defined($ARGV[0]) || die "Usage: ", basename($0). " printer1 [printer2 ..]\n";
syslog ('local7|info',basename($0)." starting ..");

while (1) {					# Loop forever, processing
  foreach my $user (@ARGV) {			# print-queues in turn.
    my (@field,@part,%slot,%tp); sleep $sleep;
    my $pop=Net::POP3->new() or next;		# Login to POP3 server and get
    my $count=$pop->login($user) or next;	# header plus 1st 15 lines of
  I:for (my $i=1;$i<=$count;$i++) {		# each message. Mark old and
      my $s=$pop->list($i) or next;		# over-size messages for
      my $u=$user."=".$pop->uidl($i) or next;	# deletion.
      $uidtime{$u}=time if ! exists $uidtime{$u};
      if ((time-$uidtime{$u})>$expire) {$uidtime{$u}=0}
      if (($s>$maxmess) or ($uidtime{$u}<1)) {	# Delete marked messages; this
        $pop->delete($i);			# mechanism picks up messages
        syslog('local7|info',"$u expired!");next# which didn't get deleted due
      }						# to server connection failure.
      my $top15=$pop->top($i,15) or next;	
      my $notify="None"; my $reply="";
      for (my $j=0;$j<99;$j++) {
        if (@$top15[$j]) {		
          (@field)=split(/=/,@$top15[$j]);
          if (defined($field[0])) {	
            if ($field[0] eq "BRO-NOTIFY" ) {chomp $field[1]; $notify=$field[1]}
            if ($field[0] eq "BRO-REPLY"  ) {chomp $field[1]; $reply =$field[1]}
            if ($field[0] eq "BRO-PARTIAL") {	# Success is notified if
              (@part)=split("/",$field[1]);	# REPLY address is supplied
              chomp $part[1]			# and NOTIFY is not "None".
            }
            if ($field[0] eq "BRO-UID") {	# Determine print-job and part
              chomp $field[1];			# thereof contained in message.
              $slot{$field[1]."=".$part[0]}=$i;
              $tp{$field[1]}=$part[1] if $part[1] eq $part[0]; 
              next I if ! defined ($tp{$field[1]});
              for (my $k=1;$k<=$tp{$field[1]};$k++) {
                next I if ! defined($slot{$field[1]."=".$k})
              }					# All parts are on server?
              my @dec; my $js=0;		# If so, get and decode them!
              for (my $k=1;$k<=$tp{$field[1]};$k++) {
                my $buf=$pop->get($slot{$field[1]."=".$k}) or next I;
                my $f=0; my $l=0; my $enc="";
                while (defined (@$buf[$l])) {
                  if (($f==0)&&(@$buf[$l]=~m/^Content-Transfer-E.+64$/)) {$f=1}
                  elsif (($f==1)&&(length(@$buf[$l])<2))                 {$f=2}
                  elsif (($f==2)&&(length(@$buf[$l])>1))      {$enc.=@$buf[$l]}
                  elsif ( $f==2 ) {
                    $enc=~tr|A-Za-z0-9+=/||cd; next I if length($enc)%4;
                    $dec[$k-1]=decode_base64($enc);
                    if (($js+=length($dec[$k-1]))<=$maxjob)              {last} 
                    $uidtime{$u}=0; next I
                  }
                  $l++
                }
              }	#$k				# If we got and decoded all
              if ($js>0) {			# parts, mark one for deletion,
                $uidtime{$u}=0;			# then print decoded string.
                my $prn=new Printer($^O=>"$user"); $prn->print(@dec);
                syslog('local7|info',"$field[1] $tp{$field[1]} => $user");
                if (($notify ne "None") && ($reply ne "")) {
                  my $smtp=Net::SMTP->new(); $smtp->mail(); $smtp->to($reply);
                  $smtp->data("Subject: Job $field[1] for Printer $user\n",
                    $js." bytes printed from $tp{$field[1]} parts!");
                  $smtp->quit
                }
              }
            }
          }
        }
      } #$j
    } #$i
    if (defined $count) {			# If a server mailbox is empty
      $pop->quit();				# clean out all records relating
      if ($count<1) {				# to it.
        foreach my $r (keys(%uidtime)) {delete $uidtime{$r} if $r=~m#^$user=#}
      }
    }
  } #$user
}

__END__

=head1 NAME

BIPserver - server for Brother-Internet-Print protocol

=head1 README

BIPserver acquires Brother-Internet-Print jobs from
a POP3 server and passes them to designated printers.

=head1 DESCRIPTION

C<BIPserver> is a simple server program for the 
Brother-Internet-Print protocol. It should be started
at boot time, and will run continually
thereafter, acquiring jobs which have been sent to
designated addresses on a POP3 server, and passing
them to corresponding printers.

The program has been designed to handle multi-part jobs,
and to accomodate unreliable connections to its
associated POP3 server. All job parts are left on the
POP3 server until a full set is available; they are
then assembled for printing and marked for deletion. No
local filespace is used. Automatic expiration of orphan
parts is also performed.

If security is an issue, the program can be extended
so as to perform user authentication based on a one-time
or other password supplied in the reply-address field of
each job.

=head1 USAGE

=over 4

=item BIPserver printer1 [printer2] .. 

=back

Mailboxes having the names of each printer designated on
the command line are accessed in turn, and job-parts
residing therein are extracted and assembled for printing.

Default values are assumed for maximum message-size (bytes),
maximum job-size (bytes), message-expire time (secs) and
sleep-time (secs); these can be adjusted via the environment
variables MAXMESS, MAXJOB, EXPIRE and SLEEP.

The POP3 server is determined using Net::Config, and login
passwords are extracted using Net::Netrc.

An appropriate Windows client program can be downloaded
from <www.brother.com>; a Unix/Linux version is available
from the author.

=head1 SCRIPT CATEGORIES

Networking
UNIX/System_administration

=head1 AUTHOR

Graham Jenkins <grahjenk@au1.ibm.com>

=head1 COPYRIGHT

Copyright (c) 2002 Graham Jenkins. All rights reserved.
This program is free software; you can redistribute it
and/or modify it under the same terms as Perl itself.

=cut
