added message forwarding code
[spider.git] / perl / DXMsg.pm
1 #!/usr/bin/perl
2 #
3 # This module impliments the message handling for a dx cluster
4 #
5 # Copyright (c) 1998 Dirk Koopman G1TLH
6 #
7 # $Id$
8 #
9 #
10 # Notes for implementors:-
11 #
12 # PC28 field 11 is the RR required flag
13 # PC28 field 12 is a VIA routing (ie it is a node call) 
14
15 package DXMsg;
16
17 @ISA = qw(DXProt DXChannel);
18
19 use DXUtil;
20 use DXChannel;
21 use DXUser;
22 use DXM;
23 use DXCluster;
24 use DXProtVars;
25 use DXProtout;
26 use DXDebug;
27 use DXLog;
28 use FileHandle;
29 use Carp;
30
31 use strict;
32 use vars qw(%work @msg $msgdir %valid %busy $maxage $last_clean
33                         @badmsg $badmsgfn $forwardfn @forward);
34
35 %work = ();                                             # outstanding jobs
36 @msg = ();                                              # messages we have
37 %busy = ();                                             # station interlocks
38 $msgdir = "$main::root/msg";    # directory contain the msgs
39 $maxage = 30 * 86400;                   # the maximum age that a message shall live for if not marked 
40 $last_clean = 0;                                # last time we did a clean
41 @forward = ();                  # msg forward table
42
43 $badmsgfn = "$msgdir/badmsg.pl";  # list of TO address we wont store
44 $forwardfn = "$msgdir/forward.pl";  # the forwarding table
45
46 %valid = (
47                   fromnode => '9,From Node',
48                   tonode => '9,To Node',
49                   to => '0,To',
50                   from => '0,From',
51                   t => '0,Msg Time,cldatetime',
52                   private => '9,Private',
53                   subject => '0,Subject',
54                   linesreq => '0,Lines per Gob',
55                   rrreq => '9,Read Confirm',
56                   origin => '0,Origin',
57                   lines => '5,Data',
58                   stream => '9,Stream No',
59                   count => '9,Gob Linecnt',
60                   file => '9,File?,yesno',
61                   gotit => '9,Got it Nodes,parray',
62                   lines => '9,Lines,parray',
63                   'read' => '9,Times read',
64                   size => '0,Size',
65                   msgno => '0,Msgno',
66                   keep => '0,Keep this?,yesno',
67                  );
68
69 # allocate a new object
70 # called fromnode, tonode, from, to, datetime, private?, subject, nolinesper  
71 sub alloc                  
72 {
73         my $pkg = shift;
74         my $self = bless {}, $pkg;
75         $self->{msgno} = shift;
76         my $to = shift;
77         #  $to =~ s/-\d+$//o;
78         $self->{to} = $to;
79         my $from = shift;
80         $from =~ s/-\d+$//o;
81         $self->{from} = uc $from;
82         $self->{t} = shift;
83         $self->{private} = shift;
84         $self->{subject} = shift;
85         $self->{origin} = shift;
86         $self->{'read'} = shift;
87         $self->{rrreq} = shift;
88         $self->{gotit} = [];
89     
90         return $self;
91 }
92
93 sub workclean
94 {
95         my $ref = shift;
96         delete $ref->{lines};
97         delete $ref->{linesreq};
98         delete $ref->{tonode};
99         delete $ref->{fromnode};
100         delete $ref->{stream};
101         delete $ref->{lines};
102         delete $ref->{file};
103         delete $ref->{count};
104 }
105
106 sub process
107 {
108         my ($self, $line) = @_;
109         my @f = split /\^/, $line;
110         my ($pcno) = $f[0] =~ /^PC(\d\d)/; # just get the number
111         
112  SWITCH: {
113                 if ($pcno == 28) {              # incoming message
114                         my $t = cltounix($f[5], $f[6]);
115                         my $stream = next_transno($f[2]);
116                         my $ref = DXMsg->alloc($stream, uc $f[3], $f[4], $t, $f[7], $f[8], $f[13], '0', $f[11]);
117                         
118                         # fill in various forwarding state variables
119                         $ref->{fromnode} = $f[2];
120                         $ref->{tonode} = $f[1];
121                         $ref->{rrreq} = $f[11];
122                         $ref->{linesreq} = $f[10];
123                         $ref->{stream} = $stream;
124                         $ref->{count} = 0;      # no of lines between PC31s
125                         dbg('msg', "new message from $f[4] to $f[3] '$f[8]' stream $stream\n");
126                         $work{"$f[2]$stream"} = $ref; # store in work
127                         $busy{$f[2]} = $ref; # set interlock
128                         $self->send(DXProt::pc30($f[2], $f[1], $stream)); # send ack
129                         last SWITCH;
130                 }
131                 
132                 if ($pcno == 29) {              # incoming text
133                         my $ref = $work{"$f[2]$f[3]"};
134                         if ($ref) {
135                                 push @{$ref->{lines}}, $f[4];
136                                 $ref->{count}++;
137                                 if ($ref->{count} >= $ref->{linesreq}) {
138                                         $self->send(DXProt::pc31($f[2], $f[1], $f[3]));
139                                         dbg('msg', "stream $f[3]: $ref->{count} lines received\n");
140                                         $ref->{count} = 0;
141                                 }
142                         }
143                         last SWITCH;
144                 }
145                 
146                 if ($pcno == 30) {              # this is a incoming subject ack
147                         my $ref = $work{$f[2]}; # note no stream at this stage
148                         if ($ref) {
149                                 delete $work{$f[2]};
150                                 $ref->{stream} = $f[3];
151                                 $ref->{count} = 0;
152                                 $ref->{linesreq} = 5;
153                                 $work{"$f[2]$f[3]"} = $ref;     # new ref
154                                 dbg('msg', "incoming subject ack stream $f[3]\n");
155                                 $busy{$f[2]} = $ref; # interlock
156                                 $ref->{lines} = [];
157                                 push @{$ref->{lines}}, ($ref->read_msg_body);
158                                 $ref->send_tranche($self);
159                         } else {
160                                 $self->send(DXProt::pc42($f[2], $f[1], $f[3])); # unknown stream
161                         } 
162                         last SWITCH;
163                 }
164                 
165                 if ($pcno == 31) {              # acknowledge a tranche of lines
166                         my $ref = $work{"$f[2]$f[3]"};
167                         if ($ref) {
168                                 dbg('msg', "tranche ack stream $f[3]\n");
169                                 $ref->send_tranche($self);
170                         } else {
171                                 $self->send(DXProt::pc42($f[2], $f[1], $f[3])); # unknown stream
172                         } 
173                         last SWITCH;
174                 }
175                 
176                 if ($pcno == 32) {              # incoming EOM
177                         dbg('msg', "stream $f[3]: EOM received\n");
178                         my $ref = $work{"$f[2]$f[3]"};
179                         if ($ref) {
180                                 $self->send(DXProt::pc33($f[2], $f[1], $f[3])); # acknowledge it
181                                 
182                                 # get the next msg no - note that this has NOTHING to do with the stream number in PC protocol
183                                 # store the file or message
184                                 # remove extraneous rubbish from the hash
185                                 # remove it from the work in progress vector
186                                 # stuff it on the msg queue
187                                 if ($ref->{lines} && @{$ref->{lines}} > 0) { # ignore messages with 0 lines
188                                         if ($ref->{file}) {
189                                                 $ref->store($ref->{lines});
190                                         } else {
191
192                                                 # does an identical message already exist?
193                                                 my $m;
194                                                 for $m (@msg) {
195                                                         if ($ref->{subject} eq $m->{subject} && $ref->{t} == $m->{t} && $ref->{from} eq $m->{from}) {
196                                                                 $ref->stop_msg($self);
197                                                                 my $msgno = $m->{msgno};
198                                                                 dbg('msg', "duplicate message to $msgno\n");
199                                                                 Log('msg', "duplicate message to $msgno");
200                                                                 return;
201                                                         }
202                                                 }
203                                                         
204                                                 # look for 'bad' to addresses 
205                                                 if (grep $ref->{to} eq $_, @badmsg) {
206                                                         $ref->stop_msg($self);
207                                                         dbg('msg', "'Bad' TO address $ref->{to}");
208                                                         Log('msg', "'Bad' TO address $ref->{to}");
209                                                         return;
210                                                 }
211
212                                                 $ref->{msgno} = next_transno("Msgno");
213                                                 push @{$ref->{gotit}}, $f[2]; # mark this up as being received
214                                                 $ref->store($ref->{lines});
215                                                 add_dir($ref);
216                                                 my $dxchan = DXChannel->get($ref->{to});
217                                                 $dxchan->send($dxchan->msg('msgnew')) if $dxchan;
218                                                 Log('msg', "Message $ref->{msgno} from $ref->{from} received from $f[2] for $ref->{to}");
219                                         }
220                                 }
221                                 $ref->stop_msg($self);
222                                 queue_msg(0);
223                         } else {
224                                 $self->send(DXProt::pc42($f[2], $f[1], $f[3])); # unknown stream
225                         }
226                         queue_msg(0);
227                         last SWITCH;
228                 }
229                 
230                 if ($pcno == 33) {              # acknowledge the end of message
231                         my $ref = $work{"$f[2]$f[3]"};
232                         if ($ref) {
233                                 if ($ref->{private}) { # remove it if it private and gone off site#
234                                         Log('msg', "Message $ref->{msgno} from $ref->{from} sent to $f[2] and deleted");
235                                         $ref->del_msg;
236                                 } else {
237                                         Log('msg', "Message $ref->{msgno} from $ref->{from} sent to $f[2]");
238                                         push @{$ref->{gotit}}, $f[2]; # mark this up as being received
239                                         $ref->store($ref->{lines});     # re- store the file
240                                 }
241                                 $ref->stop_msg($self);
242                         } else {
243                                 $self->send(DXProt::pc42($f[2], $f[1], $f[3])); # unknown stream
244                         } 
245                         queue_msg(0);
246                         last SWITCH;
247                 }
248                 
249                 if ($pcno == 40) {              # this is a file request
250                         $f[3] =~ s/\\/\//og; # change the slashes
251                         $f[3] =~ s/\.//og;      # remove dots
252                         $f[3] =~ s/^\///o;   # remove the leading /
253                         $f[3] = lc $f[3];       # to lower case;
254                         dbg('msg', "incoming file $f[3]\n");
255                         last SWITCH if $f[3] =~ /^(perl|cmd|local|src|lib|include|sys|msg|connect)/; # prevent access to executables
256                         
257                         # create any directories
258                         my @part = split /\//, $f[3];
259                         my $part;
260                         my $fn = "$main::root";
261                         pop @part;                      # remove last part
262                         foreach $part (@part) {
263                                 $fn .= "/$part";
264                                 next if -e $fn;
265                                 last SWITCH if !mkdir $fn, 0777;
266                                 dbg('msg', "created directory $fn\n");
267                         }
268                         my $stream = next_transno($f[2]);
269                         my $ref = DXMsg->alloc($stream, "$main::root/$f[3]", $self->call, time, !$f[4], $f[3], ' ', '0', '0');
270                         
271                         # forwarding variables
272                         $ref->{fromnode} = $f[1];
273                         $ref->{tonode} = $f[2];
274                         $ref->{linesreq} = $f[5];
275                         $ref->{stream} = $stream;
276                         $ref->{count} = 0;      # no of lines between PC31s
277                         $ref->{file} = 1;
278                         $work{"$f[2]$stream"} = $ref; # store in work
279                         $self->send(DXProt::pc30($f[2], $f[1], $stream)); # send ack 
280                         
281                         last SWITCH;
282                 }
283                 
284                 if ($pcno == 42) {              # abort transfer
285                         dbg('msg', "stream $f[3]: abort received\n");
286                         my $ref = $work{"$f[2]$f[3]"};
287                         if ($ref) {
288                                 $ref->stop_msg($self);
289                                 $ref = undef;
290                         }
291                         
292                         last SWITCH;
293                 }
294
295                 if ($pcno == 49) {      # global delete on subject
296                         for (@msg) {
297                                 if ($_->{subject} eq $f[2]) {
298                                         $_->del_msg();
299                                         Log('msg', "Message $_->{msgno} fully deleted by $f[1]");
300                                 }
301                         }
302                 }
303         }
304         
305         clean_old() if $main::systime - $last_clean > 3600 ; # clean the message queue
306 }
307
308
309 # store a message away on disc or whatever
310 #
311 # NOTE the second arg is a REFERENCE not a list
312 sub store
313 {
314         my $ref = shift;
315         my $lines = shift;
316         
317         # we only proceed if there are actually any lines in the file
318         if (!$lines || @{$lines} == 0) {
319                 return;
320         }
321         
322         if ($ref->{file}) {                     # a file
323                 dbg('msg', "To be stored in $ref->{to}\n");
324                 
325                 my $fh = new FileHandle "$ref->{to}", "w";
326                 if (defined $fh) {
327                         my $line;
328                         foreach $line (@{$lines}) {
329                                 print $fh "$line\n";
330                         }
331                         $fh->close;
332                         dbg('msg', "file $ref->{to} stored\n");
333                         Log('msg', "file $ref->{to} from $ref->{from} stored" );
334                 } else {
335                         confess "can't open file $ref->{to} $!";  
336                 }
337         } else {                                        # a normal message
338
339                 # attempt to open the message file
340                 my $fn = filename($ref->{msgno});
341                 
342                 dbg('msg', "To be stored in $fn\n");
343                 
344                 # now save the file, overwriting what's there, YES I KNOW OK! (I will change it if it's a problem)
345                 my $fh = new FileHandle "$fn", "w";
346                 if (defined $fh) {
347                         my $rr = $ref->{rrreq} ? '1' : '0';
348                         my $priv = $ref->{private} ? '1': '0';
349                         print $fh "=== $ref->{msgno}^$ref->{to}^$ref->{from}^$ref->{t}^$priv^$ref->{subject}^$ref->{origin}^$ref->{'read'}^$rr\n";
350                         print $fh "=== ", join('^', @{$ref->{gotit}}), "\n";
351                         my $line;
352                         $ref->{size} = 0;
353                         foreach $line (@{$lines}) {
354                                 $ref->{size} += (length $line) + 1;
355                                 print $fh "$line\n";
356                         }
357                         $fh->close;
358                         dbg('msg', "msg $ref->{msgno} stored\n");
359                         Log('msg', "msg $ref->{msgno} from $ref->{from} to $ref->{to} stored" );
360                 } else {
361                         confess "can't open msg file $fn $!";  
362                 }
363         }
364 }
365
366 # delete a message
367 sub del_msg
368 {
369         my $self = shift;
370         
371         # remove it from the active message list
372         @msg = map { $_ != $self ? $_ : () } @msg;
373         
374         # belt and braces (one day I will ask someone if this is REALLY necessary)
375         delete $self->{gotit};
376         delete $self->{list};
377         
378         # remove the file
379         unlink filename($self->{msgno});
380         dbg('msg', "deleting $self->{msgno}\n");
381 }
382
383 # clean out old messages from the message queue
384 sub clean_old
385 {
386         my $ref;
387         
388         # mark old messages for deletion
389         foreach $ref (@msg) {
390                 if (!$ref->{keep} && $ref->{t} < $main::systime - $maxage) {
391                         $ref->{deleteme} = 1;
392                         delete $ref->{gotit};
393                         delete $ref->{list};
394                         unlink filename($ref->{msgno});
395                         dbg('msg', "deleting old $ref->{msgno}\n");
396                 }
397         }
398         
399         # remove them all from the active message list
400         @msg = map { $_->{deleteme} ? () : $_ } @msg;
401         $last_clean = $main::systime;
402 }
403
404 # read in a message header
405 sub read_msg_header
406
407         my $fn = shift;
408         my $file;
409         my $line;
410         my $ref;
411         my @f;
412         my $size;
413         
414         $file = new FileHandle;
415         if (!open($file, $fn)) {
416                 print "Error reading $fn $!\n";
417                 return undef;
418         }
419         $size = -s $fn;
420         $line = <$file>;                        # first line
421         chomp $line;
422         $size -= length $line;
423         if (! $line =~ /^===/o) {
424                 print "corrupt first line in $fn ($line)\n";
425                 return undef;
426         }
427         $line =~ s/^=== //o;
428         @f = split /\^/, $line;
429         $ref = DXMsg->alloc(@f);
430         
431         $line = <$file>;                        # second line
432         chomp $line;
433         $size -= length $line;
434         if (! $line =~ /^===/o) {
435                 print "corrupt second line in $fn ($line)\n";
436                 return undef;
437         }
438         $line =~ s/^=== //o;
439         $ref->{gotit} = [];
440         @f = split /\^/, $line;
441         push @{$ref->{gotit}}, @f;
442         $ref->{size} = $size;
443         
444         close($file);
445         
446         return $ref;
447 }
448
449 # read in a message header
450 sub read_msg_body
451 {
452         my $self = shift;
453         my $msgno = $self->{msgno};
454         my $file;
455         my $line;
456         my $fn = filename($msgno);
457         my @out;
458         
459         $file = new FileHandle;
460         if (!open($file, $fn)) {
461                 print "Error reading $fn $!\n";
462                 return undef;
463         }
464         chomp (@out = <$file>);
465         close($file);
466         
467         shift @out if $out[0] =~ /^=== /;
468         shift @out if $out[0] =~ /^=== /;
469         return @out;
470 }
471
472 # send a tranche of lines to the other end
473 sub send_tranche
474 {
475         my ($self, $dxchan) = @_;
476         my @out;
477         my $to = $self->{tonode};
478         my $from = $self->{fromnode};
479         my $stream = $self->{stream};
480         my $lines = $self->{lines};
481         my ($c, $i);
482         
483         for ($i = 0, $c = $self->{count}; $i < $self->{linesreq} && $c < @$lines; $i++, $c++) {
484                 push @out, DXProt::pc29($to, $from, $stream, $lines->[$c]);
485     }
486     $self->{count} = $c;
487
488     push @out, DXProt::pc32($to, $from, $stream) if $i < $self->{linesreq};
489         $dxchan->send(@out);
490 }
491
492         
493 # find a message to send out and start the ball rolling
494 sub queue_msg
495 {
496         my $sort = shift;
497         my $call = shift;
498         my $ref;
499         my $clref;
500         my $dxchan;
501         my @nodelist = DXProt::get_all_ak1a();
502         
503         # bat down the message list looking for one that needs to go off site and whose
504         # nearest node is not busy.
505
506         dbg('msg', "queue msg ($sort)\n");
507         foreach $ref (@msg) {
508                 # firstly, is it private and unread? if so can I find the recipient
509                 # in my cluster node list offsite?
510                 if ($ref->{private}) {
511                         if ($ref->{'read'} == 0) {
512                                 $clref = DXCluster->get_exact($ref->{to});
513                                 unless ($clref) {             # otherwise look for a homenode
514                                         my $uref = DXUser->get($ref->{to});
515                                         my $hnode =  $uref->homenode if $uref;
516                                         $clref = DXCluster->get_exact($hnode) if $hnode;
517                                 }
518                                 if ($clref && !grep { $clref->{dxchan} == $_ } DXCommandmode::get_all) {
519                                         $dxchan = $clref->{dxchan};
520                                         $ref->start_msg($dxchan) if $dxchan && $clref && !get_busy($dxchan->call) && $dxchan->state eq 'normal';
521                                 }
522                         }
523                 } elsif (!$sort) {
524                         # otherwise we are dealing with a bulletin, compare the gotit list with
525                         # the nodelist up above, if there are sites that haven't got it yet
526                         # then start sending it - what happens when we get loops is anyone's
527                         # guess, use (to, from, time, subject) tuple?
528                         my $noderef;
529                         foreach $noderef (@nodelist) {
530                                 next if $noderef->call eq $main::mycall;
531                                 next if grep { $_ eq $noderef->call } @{$ref->{gotit}};
532                                 next unless $ref->forward_it($noderef->call);           # check the forwarding file
533                                 # next if $noderef->isolate;               # maybe add code for stuff originated here?
534                                 # next if DXUser->get( ${$ref->{gotit}}[0] )->isolate;  # is the origin isolated?
535                                 
536                                 # if we are here we have a node that doesn't have this message
537                                 $ref->start_msg($noderef) if !get_busy($noderef->call)  && $noderef->state eq 'normal';
538                                 last;
539                         }
540                 }
541                 
542                 # if all the available nodes are busy then stop
543                 last if @nodelist == scalar grep { get_busy($_->call) } @nodelist;
544         }
545 }
546
547 # is there a message for me?
548 sub for_me
549 {
550         my $call = uc shift;
551         my $ref;
552         
553         foreach $ref (@msg) {
554                 # is it for me, private and unread? 
555                 if ($ref->{to} eq $call && $ref->{private}) {
556                         return 1 if !$ref->{'read'};
557                 }
558         }
559         return 0;
560 }
561
562 # start the message off on its travels with a PC28
563 sub start_msg
564 {
565         my ($self, $dxchan) = @_;
566         
567         dbg('msg', "start msg $self->{msgno}\n");
568         $self->{linesreq} = 5;
569         $self->{count} = 0;
570         $self->{tonode} = $dxchan->call;
571         $self->{fromnode} = $main::mycall;
572         $busy{$dxchan->call} = $self;
573         $work{"$self->{tonode}"} = $self;
574         $dxchan->send(DXProt::pc28($self->{tonode}, $self->{fromnode}, $self->{to}, $self->{from}, $self->{t}, $self->{private}, $self->{subject}, $self->{origin}, $self->{rrreq}));
575 }
576
577 # get the ref of a busy node
578 sub get_busy
579 {
580         my $call = shift;
581         return $busy{$call};
582 }
583
584 # get the busy queue
585 sub get_all_busy
586 {
587         return values %busy;
588 }
589
590 # get the forwarding queue
591 sub get_fwq
592 {
593         return values %work;
594 }
595
596 # stop a message from continuing, clean it out, unlock interlocks etc
597 sub stop_msg
598 {
599         my ($self, $dxchan) = @_;
600         my $node = $dxchan->call;
601         
602         dbg('msg', "stop msg $self->{msgno} stream $self->{stream}\n");
603         delete $work{$node};
604         delete $work{"$node$self->{stream}"};
605         $self->workclean;
606         delete $busy{$node};
607 }
608
609 # get a new transaction number from the file specified
610 sub next_transno
611 {
612         my $name = shift;
613         $name =~ s/\W//og;                      # remove non-word characters
614         my $fn = "$msgdir/$name";
615         my $msgno;
616         
617         my $fh = new FileHandle;
618         if (sysopen($fh, $fn, O_RDWR|O_CREAT, 0666)) {
619                 $fh->autoflush(1);
620                 $msgno = $fh->getline;
621                 chomp $msgno;
622                 $msgno++;
623                 seek $fh, 0, 0;
624                 $fh->print("$msgno\n");
625                 dbg('msg', "msgno $msgno allocated for $name\n");
626                 $fh->close;
627         } else {
628                 confess "can't open $fn $!";
629         }
630         return $msgno;
631 }
632
633 # initialise the message 'system', read in all the message headers
634 sub init
635 {
636         my $dir = new FileHandle;
637         my @dir;
638         my $ref;
639
640         # load various control files
641         my @in = load_badmsg();
642         print "@in\n" if @in;
643         @in = load_forward();
644         print "@in\n" if @in;
645
646         # read in the directory
647         opendir($dir, $msgdir) or confess "can't open $msgdir $!";
648         @dir = readdir($dir);
649         closedir($dir);
650
651         @msg = ();
652         for (sort @dir) {
653                 next unless /^m\d+$/o;
654                 
655                 $ref = read_msg_header("$msgdir/$_");
656                 next unless $ref;
657                 
658                 # delete any messages to 'badmsg.pl' places
659                 if (grep $ref->{to} eq $_, @badmsg) {
660                         dbg('msg', "'Bad' TO address $ref->{to}");
661                         Log('msg', "'Bad' TO address $ref->{to}");
662                         $ref->del_msg;
663                         next;
664                 }
665
666                 # add the message to the available queue
667                 add_dir($ref); 
668         }
669 }
670
671 # add the message to the directory listing
672 sub add_dir
673 {
674         my $ref = shift;
675         confess "tried to add a non-ref to the msg directory" if !ref $ref;
676         push @msg, $ref;
677 }
678
679 # return all the current messages
680 sub get_all
681 {
682         return @msg;
683 }
684
685 # get a particular message
686 sub get
687 {
688         my $msgno = shift;
689         for (@msg) {
690                 return $_ if $_->{msgno} == $msgno;
691                 last if $_->{msgno} > $msgno;
692         }
693         return undef;
694 }
695
696 # return the official filename for a message no
697 sub filename
698 {
699         return sprintf "$msgdir/m%06d", shift;
700 }
701
702 #
703 # return a list of valid elements 
704
705
706 sub fields
707 {
708         return keys(%valid);
709 }
710
711 #
712 # return a prompt for a field
713 #
714
715 sub field_prompt
716
717         my ($self, $ele) = @_;
718         return $valid{$ele};
719 }
720
721 #
722 # send a message state machine
723 sub do_send_stuff
724 {
725         my $self = shift;
726         my $line = shift;
727         my @out;
728         
729         if ($self->state eq 'send1') {
730                 #  $DB::single = 1;
731                 confess "local var gone missing" if !ref $self->{loc};
732                 my $loc = $self->{loc};
733                 $loc->{subject} = $line;
734                 $loc->{lines} = [];
735                 $self->state('sendbody');
736                 #push @out, $self->msg('sendbody');
737                 push @out, "Enter Message /EX (^Z) to send or /ABORT (^Y) to exit";
738         } elsif ($self->state eq 'sendbody') {
739                 confess "local var gone missing" if !ref $self->{loc};
740                 my $loc = $self->{loc};
741                 if ($line eq "\032" || uc $line eq "/EX") {
742                         my $to;
743                         
744                         if (@{$loc->{lines}} > 0) {
745                                 foreach $to (@{$loc->{to}}) {
746                                         my $ref;
747                                         my $systime = $main::systime;
748                                         my $mycall = $main::mycall;
749                                         $ref = DXMsg->alloc(DXMsg::next_transno('Msgno'),
750                                                                                 uc $to,
751                                                                                 $self->call, 
752                                                                                 $systime,
753                                                                                 $loc->{private}, 
754                                                                                 $loc->{subject}, 
755                                                                                 $mycall,
756                                                                                 '0',
757                                                                                 $loc->{rrreq});
758                                         $ref->store($loc->{lines});
759                                         $ref->add_dir();
760                                         #push @out, $self->msg('sendsent', $to);
761                                         push @out, "msgno $ref->{msgno} sent to $to";
762                                         my $dxchan = DXChannel->get(uc $to);
763                                         if ($dxchan) {
764                                                 if ($dxchan->is_user()) {
765                                                         $dxchan->send("New mail has arrived for you");
766                                                 }
767                                         }
768                                 }
769                         }
770                         delete $loc->{lines};
771                         delete $loc->{to};
772                         delete $self->{loc};
773                         $self->func(undef);
774                         DXMsg::queue_msg(0);
775                         $self->state('prompt');
776                 } elsif ($line eq "\031" || uc $line eq "/ABORT" || uc $line eq "/QUIT") {
777                         #push @out, $self->msg('sendabort');
778                         push @out, "aborted";
779                         delete $loc->{lines};
780                         delete $loc->{to};
781                         delete $self->{loc};
782                         $self->func(undef);
783                         $self->state('prompt');
784                 } else {
785                         
786                         # i.e. it ain't and end or abort, therefore store the line
787                         push @{$loc->{lines}}, length($line) > 0 ? $line : " ";
788                 }
789         }
790         return (1, @out);
791 }
792
793 # return the standard directory line for this ref 
794 sub dir
795 {
796         my $ref = shift;
797         return sprintf "%6d%s%s%5d %8.8s %8.8s %-6.6s %5.5s %-30.30s", 
798                 $ref->msgno, $ref->read ? '-' : ' ', $ref->private ? 'p' : ' ', $ref->size,
799                         $ref->to, $ref->from, cldate($ref->t), ztime($ref->t), $ref->subject;
800 }
801
802 # load the forward table
803 sub load_forward
804 {
805         my @out;
806         do "$forwardfn" if -e "$forwardfn";
807         push @out, $@ if $@;
808         return @out;
809 }
810
811 # load the bad message table
812 sub load_badmsg
813 {
814         my @out;
815         do "$badmsgfn" if -e "$badmsgfn";
816         push @out, $@ if $@;
817         return @out;
818 }
819
820 #
821 # forward that message or not according to the forwarding table
822 # returns 1 for forward, 0 - to ignore
823 #
824
825 sub forward_it
826 {
827         my $ref = shift;
828         my $call = shift;
829         my $i;
830         
831         for ($i = 0; $i < @forward; $i += 5) {
832                 my ($sort, $field, $pattern, $action, $bbs) = @forward[$i..($i+4)]; 
833                 my $tested;
834                 
835                 # are we interested?
836                 last if $ref->{private} && $sort ne 'P';
837                 last if !$ref->{private} && $sort ne 'B';
838                 
839                 # select field
840                 $tested = $ref->{to} if $field eq 'T';
841                 $tested = $ref->{from} if $field eq 'F';
842                 $tested = $ref->{origin} if $field eq 'O';
843                 $tested = $ref->{subject} if $field eq 'S';
844
845                 if (!$pattern || $tested =~ m{$pattern}i) {
846                         return 0 if $action eq 'I';
847                         return 1 if !$bbs || grep $_ eq $call, @{$bbs};
848                 }
849         }
850         return 0;
851 }
852
853 no strict;
854 sub AUTOLOAD
855 {
856         my $self = shift;
857         my $name = $AUTOLOAD;
858         return if $name =~ /::DESTROY$/;
859         $name =~ s/.*:://o;
860         
861         confess "Non-existant field '$AUTOLOAD'" if !$valid{$name};
862         @_ ? $self->{$name} = shift : $self->{$name} ;
863 }
864
865 1;
866
867 __END__