fe65cdb3b204667d1931113c4036f6583fcd56a8
[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
16 package DXMsg;
17
18 use DXUtil;
19 use DXChannel;
20 use DXUser;
21 use DXM;
22 use DXProtVars;
23 use DXProtout;
24 use DXDebug;
25 use DXLog;
26 use IO::File;
27 use Fcntl;
28
29 eval {
30         require Net::SMTP;
31 };
32
33 use strict;
34
35 use vars qw($VERSION $BRANCH);
36 $VERSION = sprintf( "%d.%03d", q$Revision$ =~ /(\d+)\.(\d+)/ );
37 $BRANCH = sprintf( "%d.%03d", q$Revision$ =~ /\d+\.\d+\.(\d+)\.(\d+)/  || (0,0));
38 $main::build += $VERSION;
39 $main::branch += $BRANCH;
40
41 use vars qw(%work @msg $msgdir %valid %busy $maxage $last_clean $residencetime
42                         @badmsg @swop $swopfn $badmsgfn $forwardfn @forward $timeout $waittime
43                         $email_server $email_prog $email_from
44                     $queueinterval $lastq $importfn $minchunk $maxchunk $bulltopriv);
45
46 %work = ();                                             # outstanding jobs
47 @msg = ();                                              # messages we have
48 %busy = ();                                             # station interlocks
49 $msgdir = "$main::root/msg";    # directory contain the msgs
50 $maxage = 30 * 86400;                   # the maximum age that a message shall live for if not marked 
51 $last_clean = 0;                                # last time we did a clean
52 @forward = ();                  # msg forward table
53 @badmsg = ();                                   # bad message table
54 @swop = ();                                             # swop table
55 $timeout = 30*60;               # forwarding timeout
56 $waittime = 30*60;              # time an aborted outgoing message waits before trying again
57 $queueinterval = 1*60;          # run the queue every 1 minute
58 $lastq = 0;
59
60 $minchunk = 4800;               # minimum chunk size for a split message
61 $maxchunk = 6000;               # maximum chunk size
62 $bulltopriv = 1;                                # convert msgs with callsigns to private if they are bulls
63 $residencetime = 2*86400;       # keep deleted messages for this amount of time
64 $email_server = undef;                  # DNS address of smtp server if 'smtp'
65 $email_prog = undef;                    # program name + args for sending mail
66 $email_from = undef;                    # the from address the email will appear to be from
67
68 $badmsgfn = "$msgdir/badmsg.pl";    # list of TO address we wont store
69 $forwardfn = "$msgdir/forward.pl";  # the forwarding table
70 $swopfn = "$msgdir/swop.pl";        # the swopping table
71 $importfn = "$msgdir/import";       # import directory
72
73
74 %valid = (
75                   fromnode => '5,From Node',
76                   tonode => '5,To Node',
77                   to => '0,To',
78                   from => '0,From',
79                   t => '0,Msg Time,cldatetime',
80                   private => '5,Private,yesno',
81                   subject => '0,Subject',
82                   linesreq => '0,Lines per Gob',
83                   rrreq => '5,Read Confirm,yesno',
84                   origin => '0,Origin',
85                   lines => '5,Data',
86                   stream => '9,Stream No',
87                   count => '5,Gob Linecnt',
88                   file => '5,File?,yesno',
89                   gotit => '5,Got it Nodes,parray',
90                   lines => '5,Lines,parray',
91                   'read' => '5,Times read',
92                   size => '0,Size',
93                   msgno => '0,Msgno',
94                   keep => '0,Keep this?,yesno',
95                   lastt => '5,Last processed,cldatetime',
96                   waitt => '5,Wait until,cldatetime',
97                   delete => '5,Awaiting Delete,yesno',
98                   deletetime => '5,Deletion Time,cldatetime',
99                  );
100
101 # fix up the default sendmail if available
102 for (qw(/usr/sbin/sendmail /usr/lib/sendmail /usr/sbin/sendmail)) {
103         if (-e $_) {
104                 $email_prog = $_;
105                 last;
106         }
107 }
108
109 # allocate a new object
110 # called fromnode, tonode, from, to, datetime, private?, subject, nolinesper  
111 sub alloc                  
112 {
113         my $pkg = shift;
114         my $self = bless {}, $pkg;
115         $self->{msgno} = shift;
116         my $to = shift;
117         #  $to =~ s/-\d+$//o;
118         $self->{to} = ($to eq $main::mycall) ? $main::myalias : $to;
119         my $from = shift;
120         $self->{from} = uc $from;
121         $self->{t} = shift;
122         $self->{private} = shift;
123         $self->{subject} = shift;
124         $self->{origin} = shift;
125         $self->{'read'} = shift;
126         $self->{rrreq} = shift;
127         $self->{delete} = shift;
128         $self->{deletetime} = shift || ($self->{t} + $maxage);
129         $self->{keep} = shift;
130         $self->{gotit} = [];
131 #       $self->{lastt} = $main::systime;
132         $self->{lines} = [];
133         $self->{private} = 1 if $bulltopriv && DXUser->get_current($self->{to});
134     
135         return $self;
136 }
137
138
139 sub process
140 {
141         # this is periodic processing
142         if ($main::systime >= $lastq + $queueinterval) {
143
144                 # queue some message if the interval timer has gone off
145                 queue_msg(0);
146                 
147                 # import any messages in the import directory
148                 import_msgs();
149                 
150                 $lastq = $main::systime;
151         }
152
153         # clean the message queue
154         if ($main::systime >= $last_clean+3600) {
155                 clean_old();
156                 $last_clean = $main::systime;
157         }
158         
159         # actual remove all the 'deleted' messages in one hit.
160         # this has to be delayed until here otherwise it only does one at 
161         # a time because @msg is rewritten everytime del_msg is called.
162         my @del = grep {!$_->{tonode} && $_->{delete} && !$_->{keep} && $_->{deletetime} < $main::systime} @msg;
163         for (@del) {
164                 $_->del_msg;
165         }
166         
167 }
168
169 # incoming message
170 sub handle_28
171 {
172         my $dxchan = shift;
173         my ($tonode, $fromnode) = @_[1..2];
174
175         # sort out various extant protocol errors that occur
176         my $origin = $_[13];
177         $origin = $dxchan->call unless $origin && $origin gt ' ';
178
179         # first look for any messages in the busy queue 
180         # and cancel them this should both resolve timed out incoming messages
181         # and crossing of message between nodes, incoming messages have priority
182
183         my $ref = get_busy($fromnode);
184         if ($ref) {
185                 my $otonode = $ref->{tonode} || "unknown";
186                 dbg("Busy, stopping msgno: $ref->{msgno} $fromnode->$otonode") if isdbg('msg');
187                 $ref->stop_msg($fromnode);
188         }
189
190         my $t = cltounix($_[5], $_[6]);
191         my $stream = next_transno($fromnode);
192         $ref = DXMsg->alloc($stream, uc $_[3], $_[4], $t, $_[7], $_[8], $origin, '0', $_[11]);
193                         
194         # fill in various forwarding state variables
195         $ref->{fromnode} = $fromnode;
196         $ref->{tonode} = $tonode;
197         $ref->{rrreq} = $_[11];
198         $ref->{linesreq} = $_[10];
199         $ref->{stream} = $stream;
200         $ref->{count} = 0;                      # no of lines between PC31s
201         dbg("new message from $_[4] to $_[3] '$_[8]' stream $fromnode/$stream\n") if isdbg('msg');
202         Log('msg', "Incoming message $_[4] to $_[3] '$_[8]' origin: $origin" );
203         set_fwq($fromnode, $stream, $ref); # store in work
204         set_busy($fromnode, $ref);      # set interlock
205         $dxchan->send(DXProt::pc30($fromnode, $tonode, $stream)); # send ack
206         $ref->{lastt} = $main::systime;
207
208         # look to see whether this is a non private message sent to a known callsign
209         my $uref = DXUser->get_current($ref->{to});
210         if (is_callsign($ref->{to}) && !$ref->{private} && $uref && $uref->homenode) {
211                 $ref->{private} = 1;
212                 dbg("set bull to $ref->{to} to private") if isdbg('msg');
213                 Log('msg', "set bull to $ref->{to} to private");
214         }
215 }
216                 
217 # incoming text
218 sub handle_29
219 {
220         my $dxchan = shift;
221         my ($tonode, $fromnode, $stream) = @_[1..3];
222         
223         my $ref = get_fwq($fromnode, $stream);
224         if ($ref) {
225                 $_[4] =~ s/\%5E/^/g;
226                 if (@{$ref->{lines}}) {
227                         push @{$ref->{lines}}, $_[4];
228                 } else {
229                         # temporarily store any R: lines so that we end up with 
230                         # only the first and last ones stored.
231                         if ($_[4] =~ m|^R:\d{6}/\d{4}|) {
232                                 push @{$ref->{tempr}}, $_[4];
233                         } else {
234                                 if (exists $ref->{tempr}) {
235                                         push @{$ref->{lines}}, shift @{$ref->{tempr}};
236                                         push @{$ref->{lines}}, pop @{$ref->{tempr}} if @{$ref->{tempr}};
237                                         delete $ref->{tempr};
238                                 }
239                                 push @{$ref->{lines}}, $_[4];
240                         } 
241                 }
242                 $ref->{count}++;
243                 if ($ref->{count} >= $ref->{linesreq}) {
244                         $dxchan->send(DXProt::pc31($fromnode, $tonode, $stream));
245                         dbg("stream $stream: $ref->{count} lines received\n") if isdbg('msg');
246                         $ref->{count} = 0;
247                 }
248                 $ref->{lastt} = $main::systime;
249         } else {
250                 dbg("PC29 from unknown stream $stream from $fromnode") if isdbg('msg');
251                 $dxchan->send(DXProt::pc42($fromnode, $tonode, $stream));       # unknown stream
252         }
253 }
254                 
255 # this is a incoming subject ack
256 sub handle_30
257 {
258         my $dxchan = shift;
259         my ($tonode, $fromnode, $stream) = @_[1..3];
260
261         my $ref = get_fwq($fromnode); # note no stream at this stage
262         if ($ref) {
263                 del_fwq($fromnode);
264                 $ref->{stream} = $stream;
265                 $ref->{count} = 0;
266                 $ref->{linesreq} = 5;
267                 set_fwq($fromnode, $stream, $ref); # new ref
268                 set_busy($fromnode, $ref); # interlock
269                 dbg("incoming subject ack stream $stream\n") if isdbg('msg');
270                 $ref->{lines} = [ $ref->read_msg_body ];
271                 $ref->send_tranche($dxchan);
272                 $ref->{lastt} = $main::systime;
273         } else {
274                 dbg("PC30 from unknown stream $stream from $fromnode") if isdbg('msg');
275                 $dxchan->send(DXProt::pc42($fromnode, $tonode, $stream));       # unknown stream
276         } 
277 }
278                 
279 # acknowledge a tranche of lines
280 sub handle_31
281 {
282         my $dxchan = shift;
283         my ($tonode, $fromnode, $stream) = @_[1..3];
284
285         my $ref = get_fwq($fromnode, $stream);
286         if ($ref) {
287                 dbg("tranche ack stream $stream\n") if isdbg('msg');
288                 $ref->send_tranche($dxchan);
289                 $ref->{lastt} = $main::systime;
290         } else {
291                 dbg("PC31 from unknown stream $stream from $fromnode") if isdbg('msg');
292                 $dxchan->send(DXProt::pc42($fromnode, $tonode, $stream));       # unknown stream
293         } 
294 }
295                 
296 # incoming EOM
297 sub handle_32
298 {
299         my $dxchan = shift;
300         my ($tonode, $fromnode, $stream) = @_[1..3];
301
302         dbg("stream $stream: EOM received\n") if isdbg('msg');
303         my $ref = get_fwq($fromnode, $stream);
304         if ($ref) {
305                 $dxchan->send(DXProt::pc33($fromnode, $tonode, $stream));       # acknowledge it
306                                 
307                 # get the next msg no - note that this has NOTHING to do with the stream number in PC protocol
308                 # store the file or message
309                 # remove extraneous rubbish from the hash
310                 # remove it from the work in progress vector
311                 # stuff it on the msg queue
312                 if ($ref->{lines}) {
313                         if ($ref->{file}) {
314                                 $ref->store($ref->{lines});
315                         } else {
316
317                                 # is it too old
318                                 if ($ref->{t}+$maxage < $main::systime ) {
319                                         $ref->stop_msg($fromnode);
320                                         dbg("old message from $ref->{from} -> $ref->{to} " . atime($ref->{t}) . " ignored") if isdbg('msg');
321                                         Log('msg', "old message from $ref->{from} -> $ref->{to} " . cldatetime($ref->{t}) . " ignored");
322                                         return;
323                                 }
324
325                                 # does an identical message already exist?
326                                 my $m;
327                                 for $m (@msg) {
328                                         if (substr($ref->{subject},0,28) eq substr($m->{subject},0,28) && $ref->{t} == $m->{t} && $ref->{from} eq $m->{from} && $ref->{to} eq $m->{to}) {
329                                                 $ref->stop_msg($fromnode);
330                                                 my $msgno = $m->{msgno};
331                                                 dbg("duplicate message from $ref->{from} -> $ref->{to} to msg: $msgno") if isdbg('msg');
332                                                 Log('msg', "duplicate message from $ref->{from} -> $ref->{to} to msg: $msgno");
333                                                 return;
334                                         }
335                                 }
336
337                                 # swop addresses
338                                 $ref->swop_it($dxchan->call);
339                                                 
340                                 # look for 'bad' to addresses 
341                                 if ($ref->dump_it($dxchan->call)) {
342                                         $ref->stop_msg($fromnode);
343                                         dbg("'Bad' message $ref->{to}") if isdbg('msg');
344                                         Log('msg', "'Bad' message $ref->{to}");
345                                         return;
346                                 }
347
348                                 # check the message for bad words 
349                                 my @bad;
350                                 my @words;
351                                 @bad = BadWords::check($ref->{subject});
352                                 push @words, [$ref->{subject}, @bad] if @bad; 
353                                 for (@{$ref->{lines}}) {
354                                         @bad = BadWords::check($_);
355                                         push @words, [$_, @bad] if @bad;
356                                 }
357                                 if (@words) {
358                                         LogDbg('msg',"$ref->{from} swore: $ref->{to} origin: $ref->{origin} via " . $dxchan->call);
359                                         LogDbg('msg',"subject: $ref->{subject}");
360                                         for (@words) {
361                                                 my $r = $_;
362                                                 my $line = shift @$r;
363                                                 LogDbg('msg', "line: $line (using words: ". join(',', @$r).")");
364                                         }
365                                         $ref->stop_msg($fromnode);
366                                         return;
367                                 }
368                                                         
369                                 $ref->{msgno} = next_transno("Msgno");
370                                 push @{$ref->{gotit}}, $fromnode; # mark this up as being received
371                                 $ref->store($ref->{lines});
372                                 $ref->notify;
373                                 add_dir($ref);
374                                 Log('msg', "Message $ref->{msgno} from $ref->{from} received from $fromnode for $ref->{to}");
375                         }
376                 }
377                 $ref->stop_msg($fromnode);
378         } else {
379                 dbg("PC32 from unknown stream $stream from $fromnode") if isdbg('msg');
380                 $dxchan->send(DXProt::pc42($fromnode, $tonode, $stream));       # unknown stream
381         }
382         # queue_msg(0);
383 }
384                 
385 # acknowledge the end of message
386 sub handle_33
387 {
388         my $dxchan = shift;
389         my ($tonode, $fromnode, $stream) = @_[1..3];
390         
391         my $ref = get_fwq($fromnode, $stream);
392         if ($ref) {
393                 if ($ref->{private}) {  # remove it if it private and gone off site#
394                         Log('msg', "Message $ref->{msgno} from $ref->{from} sent to $fromnode and deleted");
395                         $ref->mark_delete;
396                 } else {
397                         Log('msg', "Message $ref->{msgno} from $ref->{from} sent to $fromnode");
398                         push @{$ref->{gotit}}, $fromnode; # mark this up as being received
399                         $ref->store($ref->{lines});     # re- store the file
400                 }
401                 $ref->stop_msg($fromnode);
402         } else {
403                 dbg("PC33 from unknown stream $stream from $fromnode") if isdbg('msg');
404                 $dxchan->send(DXProt::pc42($fromnode, $tonode, $stream));       # unknown stream
405         } 
406
407         # send next one if present
408         queue_msg(0);
409 }
410                 
411 # this is a file request
412 sub handle_40
413 {
414         my $dxchan = shift;
415         my ($tonode, $fromnode) = @_[1..2];
416         
417         $_[3] =~ s/\\/\//og;            # change the slashes
418         $_[3] =~ s/\.//og;                      # remove dots
419         $_[3] =~ s/^\///o;                      # remove the leading /
420         $_[3] = lc $_[3];                       # to lower case;
421         dbg("incoming file $_[3]\n") if isdbg('msg');
422         $_[3] = 'packclus/' . $_[3] unless $_[3] =~ /^packclus\//o;
423                         
424         # create any directories
425         my @part = split /\//, $_[3];
426         my $part;
427         my $fn = "$main::root";
428         pop @part;                                      # remove last part
429         foreach $part (@part) {
430                 $fn .= "/$part";
431                 next if -e $fn;
432                 last SWITCH if !mkdir $fn, 0777;
433                 dbg("created directory $fn\n") if isdbg('msg');
434         }
435         my $stream = next_transno($fromnode);
436         my $ref = DXMsg->alloc($stream, "$main::root/$_[3]", $dxchan->call, time, !$_[4], $_[3], ' ', '0', '0');
437                         
438         # forwarding variables
439         $ref->{fromnode} = $tonode;
440         $ref->{tonode} = $fromnode;
441         $ref->{linesreq} = $_[5];
442         $ref->{stream} = $stream;
443         $ref->{count} = 0;                      # no of lines between PC31s
444         $ref->{file} = 1;
445         $ref->{lastt} = $main::systime;
446         set_fwq($fromnode, $stream, $ref); # store in work
447         $dxchan->send(DXProt::pc30($fromnode, $tonode, $stream)); # send ack 
448 }
449                 
450 # abort transfer
451 sub handle_42
452 {
453         my $dxchan = shift;
454         my ($tonode, $fromnode, $stream) = @_[1..3];
455         
456         dbg("stream $stream: abort received\n") if isdbg('msg');
457         my $ref = get_fwq($fromnode, $stream);
458         if ($ref) {
459                 $ref->stop_msg($fromnode);
460                 $ref = undef;
461         }
462 }
463
464 # global delete on subject
465 sub handle_49
466 {
467         my $dxchan = shift;
468         my $line = shift;
469         
470         for (@msg) {
471                 if ($_->{from} eq $_[1] && $_->{subject} eq $_[2]) {
472                         $_->mark_delete;
473                         Log('msg', "Message $_->{msgno} from $_->{from} ($_->{subject}) fully deleted");
474                         DXChannel::broadcast_nodes($line, $dxchan);
475                 }
476         }
477 }
478
479
480
481 sub notify
482 {
483         my $ref = shift;
484         my $to = $ref->{to};
485         my $uref = DXUser->get_current($to);
486         my $dxchan = DXChannel::get($to);
487         if (((*Net::SMTP && $email_server) || $email_prog) && $uref && $uref->wantemail) {
488                 my $email = $uref->email;
489                 if ($email) {
490                         my @rcpt = ref $email ? @{$email} : $email;
491                         my $fromaddr = $email_from || $main::myemail;
492                         my @headers = ("To: $ref->{to}", 
493                                                    "From: $fromaddr",
494                                                    "Subject: [DXSpider: $ref->{from}] $ref->{subject}", 
495                                                    "X-DXSpider-To: $ref->{to}",
496                                                    "X-DXSpider-From: $ref->{from}\@$ref->{origin}", 
497                                                    "X-DXSpider-Gateway: $main::mycall"
498                                                   );
499                         my @data = ("Msgno: $ref->{msgno} To: $to From: $ref->{from}\@$ref->{origin} Gateway: $main::mycall", 
500                                                 "", 
501                                                 $ref->read_msg_body
502                                            );
503                         my $msg;
504                         undef $!;
505                         if (*Net::SMTP && $email_server) {
506                                 $msg = Net::SMTP->new($email_server);
507                                 if ($msg) {
508                                         $msg->mail($fromaddr);
509                                         $msg->to(@rcpt);
510                                         $msg->data(map {"$_\n"} @headers, '', @data);
511                                         $msg->quit;
512                                 }
513                         } elsif ($email_prog) {
514                                 $msg = new IO::File "|$email_prog " . join(' ', @rcpt);
515                                 if ($msg) {
516                                         print $msg map {"$_\r\n"} @headers, '', @data, '.';
517                                         $msg->close;
518                                 }
519                         }
520                         dbg("email forwarding error $!") if isdbg('msg') && !$msg && defined $!; 
521                 }
522         }
523         $dxchan->send($dxchan->msg('m9')) if $dxchan && $dxchan->is_user;
524 }
525
526 # store a message away on disc or whatever
527 #
528 # NOTE the second arg is a REFERENCE not a list
529 sub store
530 {
531         my $ref = shift;
532         my $lines = shift;
533
534         if ($ref->{file}) {                     # a file
535                 dbg("To be stored in $ref->{to}\n") if isdbg('msg');
536                 
537                 my $fh = new IO::File "$ref->{to}", "w";
538                 if (defined $fh) {
539                         my $line;
540                         foreach $line (@{$lines}) {
541                                 print $fh "$line\n";
542                         }
543                         $fh->close;
544                         dbg("file $ref->{to} stored\n") if isdbg('msg');
545                         Log('msg', "file $ref->{to} from $ref->{from} stored" );
546                 } else {
547                         confess "can't open file $ref->{to} $!";  
548                 }
549         } else {                                        # a normal message
550
551                 # attempt to open the message file
552                 my $fn = filename($ref->{msgno});
553                 
554                 dbg("To be stored in $fn\n") if isdbg('msg');
555                 
556                 # now save the file, overwriting what's there, YES I KNOW OK! (I will change it if it's a problem)
557                 my $fh = new IO::File "$fn", "w";
558                 if (defined $fh) {
559                         my $rr = $ref->{rrreq} ? '1' : '0';
560                         my $priv = $ref->{private} ? '1': '0';
561                         my $del = $ref->{delete} ? '1' : '0';
562                         my $delt = $ref->{deletetime} || ($ref->{t} + $maxage);
563                         my $keep = $ref->{keep} || '0';
564                         print $fh "=== $ref->{msgno}^$ref->{to}^$ref->{from}^$ref->{t}^$priv^$ref->{subject}^$ref->{origin}^$ref->{'read'}^$rr^$del^$delt^$keep\n";
565                         print $fh "=== ", join('^', @{$ref->{gotit}}), "\n";
566                         my $line;
567                         $ref->{size} = 0;
568                         foreach $line (@{$lines}) {
569                                 $line =~ s/[\x00-\x08\x0a-\x1f\x80-\x9f]/./g;
570                                 $ref->{size} += (length $line) + 1;
571                                 print $fh "$line\n";
572                         }
573                         $fh->close;
574                         dbg("msg $ref->{msgno} stored\n") if isdbg('msg');
575                         Log('msg', "msg $ref->{msgno} from $ref->{from} to $ref->{to} stored" );
576                 } else {
577                         confess "can't open msg file $fn $!";  
578                 }
579         }
580
581 }
582
583 # delete a message
584 sub del_msg
585 {
586         my $self = shift;
587         my $dxchan = shift;
588         my $call = '';
589         $call = ' by ' . $dxchan->call if $dxchan;
590         
591         if ($self->{tonode}) {
592                 $self->{delete}++;
593                 $self->{deletetime} = 0;
594                 dbg("Msgno $self->{msgno} but marked as expunged$call") if isdbg('msg');
595         } else {
596                 # remove it from the active message list
597                 @msg = grep { $_ != $self } @msg;
598
599                 Log('msg', "Msgno $self->{msgno} expunged$call");
600                 dbg("Msgno $self->{msgno} expunged$call") if isdbg('msg');
601                 
602                 # remove the file
603                 unlink filename($self->{msgno});
604         }
605 }
606
607 sub mark_delete
608 {
609         my $ref = shift;
610         my $t = shift;
611
612         return if $ref->{keep};
613         
614         $t = $main::systime + $residencetime unless defined $t;
615         
616         $ref->{delete}++;
617         $ref->{deletetime} = $t;
618         $ref->store( [$ref->read_msg_body] );
619 }
620
621 sub unmark_delete
622 {
623         my $ref = shift;
624         my $t = shift;
625         $ref->{delete} = 0;
626         $ref->{deletetime} = 0;
627 }
628
629 # clean out old messages from the message queue
630 sub clean_old
631 {
632         my $ref;
633         
634         # mark old messages for deletion
635         foreach $ref (@msg) {
636                 if (ref($ref) && !$ref->{keep} && $ref->{deletetime} < $main::systime) {
637
638                         # this is for IMMEDIATE destruction
639                         $ref->{delete}++;
640                         $ref->{deletetime} = 0;
641                 }
642         }
643 }
644
645 # read in a message header
646 sub read_msg_header
647
648         my $fn = shift;
649         my $file;
650         my $line;
651         my $ref;
652         my @f;
653         my $size;
654         
655         $file = new IO::File "$fn";
656         if (!$file) {
657             dbg("Error reading $fn $!");
658             Log('err', "Error reading $fn $!");
659                 return undef;
660         }
661         $size = -s $fn;
662         $line = <$file>;                        # first line
663         if ($size == 0 || !$line) {
664             dbg("Empty $fn $!");
665             Log('err', "Empty $fn $!");
666                 return undef;
667         }
668         chomp $line;
669         $size -= length $line;
670         if (! $line =~ /^===/o) {
671                 dbg("corrupt first line in $fn ($line)");
672                 Log('err', "corrupt first line in $fn ($line)");
673                 return undef;
674         }
675         $line =~ s/^=== //o;
676         @f = split /\^/, $line;
677         $ref = DXMsg->alloc(@f);
678         
679         $line = <$file>;                        # second line
680         chomp $line;
681         $size -= length $line;
682         if (! $line =~ /^===/o) {
683             dbg("corrupt second line in $fn ($line)");
684             Log('err', "corrupt second line in $fn ($line)");
685                 return undef;
686         }
687         $line =~ s/^=== //o;
688         $ref->{gotit} = [];
689         @f = split /\^/, $line;
690         push @{$ref->{gotit}}, @f;
691         $ref->{size} = $size;
692         
693         close($file);
694         
695         return $ref;
696 }
697
698 # read in a message header
699 sub read_msg_body
700 {
701         my $self = shift;
702         my $msgno = $self->{msgno};
703         my $file;
704         my $line;
705         my $fn = filename($msgno);
706         my @out;
707         
708         $file = new IO::File;
709         if (!open($file, $fn)) {
710                 dbg("Error reading $fn $!");
711                 Log('err' ,"Error reading $fn $!");
712                 return ();
713         }
714         @out = map {chomp; $_} <$file>;
715         close($file);
716         
717         shift @out if $out[0] =~ /^=== /;
718         shift @out if $out[0] =~ /^=== /;
719         return @out;
720 }
721
722 # send a tranche of lines to the other end
723 sub send_tranche
724 {
725         my ($self, $dxchan) = @_;
726         my @out;
727         my $to = $self->{tonode};
728         my $from = $self->{fromnode};
729         my $stream = $self->{stream};
730         my $lines = $self->{lines};
731         my ($c, $i);
732         
733         for ($i = 0, $c = $self->{count}; $i < $self->{linesreq} && $c < @$lines; $i++, $c++) {
734                 push @out, DXProt::pc29($to, $from, $stream, $lines->[$c]);
735     }
736     $self->{count} = $c;
737
738     push @out, DXProt::pc32($to, $from, $stream) if $i < $self->{linesreq};
739         $dxchan->send(@out);
740 }
741
742         
743 # find a message to send out and start the ball rolling
744 sub queue_msg
745 {
746         my $sort = shift;
747         my $ref;
748         my $clref;
749         
750         # bat down the message list looking for one that needs to go off site and whose
751         # nearest node is not busy.
752
753         dbg("queue msg ($sort)\n") if isdbg('msg');
754         my @nodelist = DXChannel::get_all_nodes;
755         foreach $ref (@msg) {
756
757                 # ignore 'delayed' messages until their waiting time has expired
758                 if (exists $ref->{waitt}) {
759                         next if $ref->{waitt} > $main::systime;
760                         delete $ref->{waitt};
761                 } 
762
763                 # any time outs?
764                 if (exists $ref->{lastt} && $main::systime >= $ref->{lastt} + $timeout) {
765                         my $node = $ref->{tonode};
766                         dbg("Timeout, stopping msgno: $ref->{msgno} -> $node") if isdbg('msg');
767                         Log('msg', "Timeout, stopping msgno: $ref->{msgno} -> $node");
768                         $ref->stop_msg($node);
769                         
770                         # delay any outgoing messages that fail
771                         $ref->{waitt} = $main::systime + $waittime + int rand(120) if $node ne $main::mycall;
772                         delete $ref->{lastt};
773                         next;
774                 }
775
776                 # is it being sent anywhere currently?
777                 next if $ref->{tonode};           # ignore it if it already being processed
778                 
779                 # is it awaiting deletion?
780                 next if $ref->{delete};
781                 
782                 # firstly, is it private and unread? if so can I find the recipient
783                 # in my cluster node list offsite?
784
785                 # deal with routed private messages
786                 my $dxchan;
787                 if ($ref->{private}) {
788                         next if $ref->{'read'};           # if it is read, it is stuck here
789                         $clref = Route::get($ref->{to});
790                         if ($clref) {
791                                 $dxchan = $clref->dxchan;
792                                 if ($dxchan) {
793                                         if ($dxchan->is_node) {
794                                                 next if $clref->call eq $main::mycall;  # i.e. it lives here
795                                                 $ref->start_msg($dxchan) if !get_busy($dxchan->call)  && $dxchan->state eq 'normal';
796                                         }
797                                 } else {
798                                         dbg("Route: No dxchan for $ref->{to} " . ref($clref) ) if isdbg('msg');
799                                 }
800                         }
801                 } else {
802                         
803                         # otherwise we are dealing with a bulletin or forwarded private message
804                         # compare the gotit list with
805                         # the nodelist up above, if there are sites that haven't got it yet
806                         # then start sending it - what happens when we get loops is anyone's
807                         # guess, use (to, from, time, subject) tuple?
808                         foreach $dxchan (@nodelist) {
809                                 my $call = $dxchan->call;
810                                 next unless $call;
811                                 next if $call eq $main::mycall;
812                                 next if ref $ref->{gotit} && grep $_ eq $call, @{$ref->{gotit}};
813                                 next unless $ref->forward_it($call);           # check the forwarding file
814                                 next if $ref->{tonode};           # ignore it if it already being processed
815                                 
816                                 # if we are here we have a node that doesn't have this message
817                                 if (!get_busy($call)  && $dxchan->state eq 'normal') {
818                                         $ref->start_msg($dxchan);
819                                         last;
820                                 }
821                         }
822                 }
823
824                 # if all the available nodes are busy then stop
825                 last if @nodelist == scalar grep { get_busy($_->call) } @nodelist;
826         }
827
828         
829 }
830
831 # is there a message for me?
832 sub for_me
833 {
834         my $call = uc shift;
835         my $ref;
836         my $count;
837         
838         foreach $ref (@msg) {
839                 # is it for me, private and unread? 
840                 if ($ref->{to} eq $call && $ref->{private}) {
841                    $count++ unless $ref->{'read'} || $ref->{delete};
842                 }
843         }
844         return $count;
845 }
846
847 # start the message off on its travels with a PC28
848 sub start_msg
849 {
850         my ($self, $dxchan) = @_;
851         
852         confess("trying to start started msg $self->{msgno} nodes: $self->{fromnode} -> $self->{tonode}") if $self->{tonode};
853         dbg("start msg $self->{msgno}\n") if isdbg('msg');
854         $self->{linesreq} = 10;
855         $self->{count} = 0;
856         $self->{tonode} = $dxchan->call;
857         $self->{fromnode} = $main::mycall;
858         set_busy($self->{tonode}, $self);
859         set_fwq($self->{tonode}, undef, $self);
860         $self->{lastt} = $main::systime;
861         my ($fromnode, $origin);
862         $fromnode = $self->{fromnode};
863         $origin = $self->{origin};
864         $dxchan->send(DXProt::pc28($self->{tonode}, $fromnode, $self->{to}, $self->{from}, $self->{t}, $self->{private}, $self->{subject}, $origin, $self->{rrreq}));
865 }
866
867 # get the ref of a busy node
868 sub get_busy
869 {
870         my $call = shift;
871         return $busy{$call};
872 }
873
874 sub set_busy
875 {
876         my $call = shift;
877         return $busy{$call} = shift;
878 }
879
880 sub del_busy
881 {
882         my $call = shift;
883         return delete $busy{$call};
884 }
885
886 # get the whole busy queue
887 sub get_all_busy
888 {
889         return keys %busy;
890 }
891
892 # get a forwarding queue entry
893 sub get_fwq
894 {
895         my $call = shift;
896         my $stream = shift || '0';
897         return $work{"$call,$stream"};
898 }
899
900 # delete a forwarding queue entry
901 sub del_fwq
902 {
903         my $call = shift;
904         my $stream = shift || '0';
905         return delete $work{"$call,$stream"};
906 }
907
908 # set a fwq entry
909 sub set_fwq
910 {
911         my $call = shift;
912         my $stream = shift || '0';
913         return $work{"$call,$stream"} = shift;
914 }
915
916 # get the whole forwarding queue
917 sub get_all_fwq
918 {
919         return keys %work;
920 }
921
922 # stop a message from continuing, clean it out, unlock interlocks etc
923 sub stop_msg
924 {
925         my $self = shift;
926         my $node = shift;
927         my $stream = $self->{stream};
928         
929         
930         dbg("stop msg $self->{msgno} -> node $node\n") if isdbg('msg');
931         del_fwq($node, $stream);
932         $self->workclean;
933         del_busy($node);
934 }
935
936 sub workclean
937 {
938         my $ref = shift;
939         delete $ref->{lines};
940         delete $ref->{linesreq};
941         delete $ref->{tonode};
942         delete $ref->{fromnode};
943         delete $ref->{stream};
944         delete $ref->{file};
945         delete $ref->{count};
946         delete $ref->{tempr};
947         delete $ref->{lastt};
948         delete $ref->{waitt};
949 }
950
951 # get a new transaction number from the file specified
952 sub next_transno
953 {
954         my $name = shift;
955         $name =~ s/\W//og;                      # remove non-word characters
956         my $fn = "$msgdir/$name";
957         my $msgno;
958         
959         my $fh = new IO::File;
960         if (sysopen($fh, $fn, O_RDWR|O_CREAT, 0666)) {
961                 $fh->autoflush(1);
962                 $msgno = $fh->getline || '0';
963                 chomp $msgno;
964                 $msgno++;
965                 seek $fh, 0, 0;
966                 $fh->print("$msgno\n");
967                 dbg("msgno $msgno allocated for $name\n") if isdbg('msg');
968                 $fh->close;
969         } else {
970                 confess "can't open $fn $!";
971         }
972         return $msgno;
973 }
974
975 # initialise the message 'system', read in all the message headers
976 sub init
977 {
978         my $dir = new IO::File;
979         my @dir;
980         my $ref;
981                 
982         # load various control files
983         dbg("load badmsg: " . (load_badmsg() or "Ok"));
984         dbg("load forward: " . (load_forward() or "Ok"));
985         dbg("load swop: " . (load_swop() or "Ok"));
986
987         # read in the directory
988         opendir($dir, $msgdir) or confess "can't open $msgdir $!";
989         @dir = readdir($dir);
990         closedir($dir);
991
992         @msg = ();
993         for (sort @dir) {
994                 next unless /^m\d\d\d\d\d\d$/;
995                 
996                 $ref = read_msg_header("$msgdir/$_");
997                 unless ($ref) {
998                         dbg("Deleting $_");
999                         Log('err', "Deleting $_");
1000                         unlink "$msgdir/$_";
1001                         next;
1002                 }
1003                 
1004                 # delete any messages to 'badmsg.pl' places
1005                 if ($ref->dump_it('')) {
1006                         dbg("'Bad' TO address $ref->{to}") if isdbg('msg');
1007                         Log('msg', "'Bad' TO address $ref->{to}");
1008                         $ref->del_msg;
1009                         next;
1010                 }
1011
1012                 # add the message to the available queue
1013                 add_dir($ref); 
1014         }
1015 }
1016
1017 # add the message to the directory listing
1018 sub add_dir
1019 {
1020         my $ref = shift;
1021         confess "tried to add a non-ref to the msg directory" if !ref $ref;
1022         push @msg, $ref;
1023 }
1024
1025 # return all the current messages
1026 sub get_all
1027 {
1028         return @msg;
1029 }
1030
1031 # get a particular message
1032 sub get
1033 {
1034         my $msgno = shift;
1035         for (@msg) {
1036                 return $_ if $_->{msgno} == $msgno;
1037                 last if $_->{msgno} > $msgno;
1038         }
1039         return undef;
1040 }
1041
1042 # return the official filename for a message no
1043 sub filename
1044 {
1045         return sprintf "$msgdir/m%06d", shift;
1046 }
1047
1048 #
1049 # return a list of valid elements 
1050
1051
1052 sub fields
1053 {
1054         return keys(%valid);
1055 }
1056
1057 #
1058 # return a prompt for a field
1059 #
1060
1061 sub field_prompt
1062
1063         my ($self, $ele) = @_;
1064         return $valid{$ele};
1065 }
1066
1067 #
1068 # send a message state machine
1069 sub do_send_stuff
1070 {
1071         my $self = shift;
1072         my $line = shift;
1073         my @out;
1074         
1075         if ($self->state eq 'send1') {
1076                 #  $DB::single = 1;
1077                 confess "local var gone missing" if !ref $self->{loc};
1078                 my $loc = $self->{loc};
1079                 if (my @ans = BadWords::check($line)) {
1080                         $self->{badcount} += @ans;
1081                         Log('msg', $self->call . " used badwords: @ans to @{$loc->{to}} in msg");
1082                         $loc->{reject}++;
1083                 }
1084                 $loc->{subject} = $line;
1085                 $loc->{lines} = [];
1086                 $self->state('sendbody');
1087                 #push @out, $self->msg('sendbody');
1088                 push @out, $self->msg('m8');
1089         } elsif ($self->state eq 'sendbody') {
1090                 confess "local var gone missing" if !ref $self->{loc};
1091                 my $loc = $self->{loc};
1092                 if ($line eq "\032" || $line eq '%1A' || uc $line eq "/EX") {
1093                         my $to;
1094                         unless ($loc->{reject}) {
1095                                 foreach $to (@{$loc->{to}}) {
1096                                         my $ref;
1097                                         my $systime = $main::systime;
1098                                         my $mycall = $main::mycall;
1099                                         $ref = DXMsg->alloc(DXMsg::next_transno('Msgno'),
1100                                                                                 uc $to,
1101                                                                                 exists $loc->{from} ? $loc->{from} : $self->call, 
1102                                                                                 $systime,
1103                                                                                 $loc->{private}, 
1104                                                                                 $loc->{subject}, 
1105                                                                                 exists $loc->{origin} ? $loc->{origin} : $mycall,
1106                                                                                 '0',
1107                                                                                 $loc->{rrreq});
1108                                         $ref->swop_it($self->call);
1109                                         $ref->store($loc->{lines});
1110                                         $ref->add_dir();
1111                                         push @out, $self->msg('m11', $ref->{msgno}, $to);
1112                                         #push @out, "msgno $ref->{msgno} sent to $to";
1113                                         $ref->notify;
1114                                 }
1115                         } else {
1116                                 LogDbg('msg', $self->call . " swore to @{$loc->{to}} subject: '$loc->{subject}' in msg, REJECTED");
1117                         }
1118                         
1119                         delete $loc->{lines};
1120                         delete $loc->{to};
1121                         delete $self->{loc};
1122                         $self->func(undef);
1123                         
1124                         $self->state('prompt');
1125                 } elsif ($line eq "\031" || uc $line eq "/ABORT" || uc $line eq "/QUIT") {
1126                         #push @out, $self->msg('sendabort');
1127                         push @out, $self->msg('m10');
1128                         delete $loc->{lines};
1129                         delete $loc->{to};
1130                         delete $self->{loc};
1131                         $self->func(undef);
1132                         $self->state('prompt');
1133                 } elsif ($line =~ m|^/+\w+|) {
1134                         # this is a command that you want display for your own reference
1135                         # or if it has TWO slashes is a command 
1136                         $line =~ s|^/||;
1137                         my $store = $line =~ s|^/+||;
1138                         my @in = $self->run_cmd($line);
1139                         push @out, @in;
1140                         if ($store) {
1141                                 foreach my $l (@in) {
1142                                         if (my @ans = BadWords::check($l)) {
1143                                                 $self->{badcount} += @ans;
1144                                                 Log('msg', $self->call . " used badwords: @ans to @{$loc->{to}} subject: '$loc->{subject}' in msg") unless $loc->{reject};
1145                                                 Log('msg', "line: $l");
1146                                                 $loc->{reject}++;
1147                                         } 
1148                                         push @{$loc->{lines}}, length($l) > 0 ? $l : " ";
1149                                 }
1150                         }
1151                 } else {
1152                         if (my @ans = BadWords::check($line)) {
1153                                 $self->{badcount} += @ans;
1154                                 Log('msg', $self->call . " used badwords: @ans to @{$loc->{to}} subject: '$loc->{subject}' in msg") unless $loc->{reject};
1155                                 Log('msg', "line: $line");
1156                                 $loc->{reject}++;
1157                         }
1158
1159                         if ($loc->{lines} && @{$loc->{lines}}) {
1160                                 push @{$loc->{lines}}, length($line) > 0 ? $line : " ";
1161                         } else {
1162                                 # temporarily store any R: lines so that we end up with 
1163                                 # only the first and last ones stored.
1164                                 if ($line =~ m|^R:\d{6}/\d{4}|) {
1165                                         push @{$loc->{tempr}}, $line;
1166                                 } else {
1167                                         if (exists $loc->{tempr}) {
1168                                                 push @{$loc->{lines}}, shift @{$loc->{tempr}};
1169                                                 push @{$loc->{lines}}, pop @{$loc->{tempr}} if @{$loc->{tempr}};
1170                                                 delete $loc->{tempr};
1171                                         }
1172                                         push @{$loc->{lines}}, length($line) > 0 ? $line : " ";
1173                                 } 
1174                         }
1175                         
1176                         # i.e. it ain't and end or abort, therefore store the line
1177                 }
1178         }
1179         return @out;
1180 }
1181
1182 # return the standard directory line for this ref 
1183 sub dir
1184 {
1185         my $ref = shift;
1186         my $flag = $ref->{private} && $ref->{read} ? '-' : ' ';
1187         if ($ref->{keep}) {
1188                 $flag = '!';
1189         } elsif ($ref->{delete}) {
1190                 $flag = $ref->{deletetime} > $main::systime ? 'D' : 'E'; 
1191         }
1192         return sprintf("%6d%s%s%5d %8.8s %8.8s %-6.6s %5.5s %-30.30s", 
1193                                    $ref->{msgno}, $flag, $ref->{private} ? 'p' : ' ', 
1194                                    $ref->{size}, $ref->{to}, $ref->{from}, cldate($ref->{t}), 
1195                                    ztime($ref->{t}), $ref->{subject});
1196 }
1197
1198 # load the forward table
1199 sub load_forward
1200 {
1201         my @out;
1202         my $s = readfilestr($forwardfn);
1203         if ($s) {
1204                 eval $s;
1205                 push @out, $@ if $@;
1206         }
1207         return @out;
1208 }
1209
1210 # load the bad message table
1211 sub load_badmsg
1212 {
1213         my @out;
1214         my $s = readfilestr($badmsgfn);
1215         if ($s) {
1216                 eval $s;
1217                 push @out, $@ if $@;
1218         }
1219         return @out;
1220 }
1221
1222 # load the swop message table
1223 sub load_swop
1224 {
1225         my @out;
1226         my $s = readfilestr($swopfn);
1227         if ($s) {
1228                 eval $s;
1229                 push @out, $@ if $@;
1230         }
1231         return @out;
1232 }
1233
1234 #
1235 # forward that message or not according to the forwarding table
1236 # returns 1 for forward, 0 - to ignore
1237 #
1238
1239 sub forward_it
1240 {
1241         my $ref = shift;
1242         my $call = shift;
1243         my $i;
1244         
1245         for ($i = 0; $i < @forward; $i += 5) {
1246                 my ($sort, $field, $pattern, $action, $bbs) = @forward[$i..($i+4)]; 
1247                 my $tested;
1248                 
1249                 # are we interested?
1250                 next if $ref->{private} && $sort ne 'P';
1251                 next if !$ref->{private} && $sort ne 'B';
1252                 
1253                 # select field
1254                 $tested = $ref->{to} if $field eq 'T';
1255                 $tested = $ref->{from} if $field eq 'F';
1256                 $tested = $ref->{origin} if $field eq 'O';
1257                 $tested = $ref->{subject} if $field eq 'S';
1258
1259                 if (!$pattern || $tested =~ m{$pattern}i) {
1260                         return 0 if $action eq 'I';
1261                         return 1 if !$bbs || grep $_ eq $call, @{$bbs};
1262                 }
1263         }
1264         return 0;
1265 }
1266
1267 #
1268 # look down the forward table to see whether this is a valid bull
1269 # or not (ie it will forward somewhere even if it is only here)
1270 #
1271 sub valid_bull_addr
1272 {
1273         my $call = shift;
1274         my $i;
1275         
1276         unless (@forward) {
1277                 return 1 if $call =~ /^ALL/;
1278                 return 1 if $call =~ /^DX/;
1279                 return 0;
1280         }
1281         
1282         for ($i = 0; $i < @forward; $i += 5) {
1283                 my ($sort, $field, $pattern, $action, $bbs) = @forward[$i..($i+4)]; 
1284                 if ($field eq 'T') {
1285                         if (!$pattern || $call =~ m{$pattern}i) {
1286                                 return 1;
1287                         }
1288                 }
1289         }
1290         return 0;
1291 }
1292
1293 sub dump_it
1294 {
1295         my $ref = shift;
1296         my $call = shift;
1297         my $i;
1298         
1299         for ($i = 0; $i < @badmsg; $i += 3) {
1300                 my ($sort, $field, $pattern) = @badmsg[$i..($i+2)]; 
1301                 my $tested;
1302                 
1303                 # are we interested?
1304                 next if $ref->{private} && $sort ne 'P';
1305                 next if !$ref->{private} && $sort ne 'B';
1306                 
1307                 # select field
1308                 $tested = $ref->{to} if $field eq 'T';
1309                 $tested = $ref->{from} if $field eq 'F';
1310                 $tested = $ref->{origin} if $field eq 'O';
1311                 $tested = $ref->{subject} if $field eq 'S';
1312                 $tested = $call if $field eq 'I';
1313
1314                 if (!$pattern || $tested =~ m{$pattern}i) {
1315                         return 1;
1316                 }
1317         }
1318         return 0;
1319 }
1320
1321 sub swop_it
1322 {
1323         my $ref = shift;
1324         my $call = shift;
1325         my $i;
1326         my $count = 0;
1327         
1328         for ($i = 0; $i < @swop; $i += 5) {
1329                 my ($sort, $field, $pattern, $tfield, $topattern) = @swop[$i..($i+4)]; 
1330                 my $tested;
1331                 my $swop;
1332                 my $old;
1333                 
1334                 # are we interested?
1335                 next if $ref->{private} && $sort ne 'P';
1336                 next if !$ref->{private} && $sort ne 'B';
1337                 
1338                 # select field
1339                 $tested = $ref->{to} if $field eq 'T';
1340                 $tested = $ref->{from} if $field eq 'F';
1341                 $tested = $ref->{origin} if $field eq 'O';
1342                 $tested = $ref->{subject} if $field eq 'S';
1343
1344                 # select swop field
1345                 $old = $swop = $ref->{to} if $tfield eq 'T';
1346                 $old = $swop = $ref->{from} if $tfield eq 'F';
1347                 $old = $swop = $ref->{origin} if $tfield eq 'O';
1348                 $old = $swop = $ref->{subject} if $tfield eq 'S';
1349
1350                 if ($tested =~ m{$pattern}i) {
1351                         if ($tested eq $swop) {
1352                                 $swop =~ s{$pattern}{$topattern}i;
1353                         } else {
1354                                 $swop = $topattern;
1355                         }
1356                         Log('msg', "Msg $ref->{msgno}: $tfield $old -> $swop");
1357                         Log('dbg', "Msg $ref->{msgno}: $tfield $old -> $swop");
1358                         $ref->{to} = $swop if $tfield eq 'T';
1359                         $ref->{from} = $swop if $tfield eq 'F';
1360                         $ref->{origin} = $swop if $tfield eq 'O';
1361                         $ref->{subject} = $swop if $tfield eq 'S';
1362                         ++$count;
1363                 }
1364         }
1365         return $count;
1366 }
1367
1368 # import any msgs in the import directory
1369 # the messages are in BBS format (but may have cluster extentions
1370 # so SB UK < GB7TLH is legal
1371 sub import_msgs
1372 {
1373         # are there any to do in this directory?
1374         return unless -d $importfn;
1375         unless (opendir(DIR, $importfn)) {
1376                 dbg("can\'t open $importfn $!") if isdbg('msg');
1377                 Log('msg', "can\'t open $importfn $!");
1378                 return;
1379         } 
1380
1381         my @names = readdir(DIR);
1382         closedir(DIR);
1383         my $name;
1384         foreach $name (@names) {
1385                 next if $name =~ /^\./;
1386                 my $splitit = $name =~ /^split/;
1387                 my $fn = "$importfn/$name";
1388                 next unless -f $fn;
1389                 unless (open(MSG, $fn)) {
1390                         dbg("can\'t open import file $fn $!") if isdbg('msg');
1391                         Log('msg', "can\'t open import file $fn $!");
1392                         unlink($fn);
1393                         next;
1394                 }
1395                 my @msg = map { chomp; $_ } <MSG>;
1396                 close(MSG);
1397                 unlink($fn);
1398                 my @out = import_one($main::me, \@msg, $splitit);
1399                 Log('msg', @out);
1400         }
1401 }
1402
1403 # import one message as a list in bbs (as extended) mode
1404 # takes a reference to an array containing the whole message
1405 sub import_one
1406 {
1407         my $dxchan = shift;
1408         my $ref = shift;
1409         my $splitit = shift;
1410         my $private = '1';
1411         my $rr = '0';
1412         my $notincalls = 1;
1413         my $from = $dxchan->call;
1414         my $origin = $main::mycall;
1415         my @to;
1416         my @out;
1417                                 
1418         # first line;
1419         my $line = shift @$ref;
1420         my @f = split /([\s\@\$])/, $line;
1421         @f = map {s/\s+//g; length $_ ? $_ : ()} @f;
1422
1423         unless (@f && $f[0] =~ /^(:?S|SP|SB|SEND)$/ ) {
1424                 my $m = "invalid first line in import '$line'";
1425                 dbg($m) if isdbg('msg');
1426                 return (1, $m);
1427         }
1428         while (@f) {
1429                 my $f = uc shift @f;
1430                 next if $f eq 'SEND';
1431
1432                 # private / noprivate / rr
1433                 if ($notincalls && ($f eq 'B' || $f eq 'SB' || $f =~ /^NOP/oi)) {
1434                         $private = '0';
1435                 } elsif ($notincalls && ($f eq 'P' || $f eq 'SP' || $f =~ /^PRI/oi)) {
1436                         ;
1437                 } elsif ($notincalls && ($f eq 'RR')) {
1438                         $rr = '1';
1439                 } elsif (($f =~ /^[\@\.\#\$]$/ || $f eq '.#') && @f) {       # this is bbs syntax, for AT
1440                         shift @f;
1441                 } elsif ($f eq '<' && @f) {     # this is bbs syntax  for from call
1442                         $from = uc shift @f;
1443                 } elsif ($f =~ /^\$/) {     # this is bbs syntax  for a bid
1444                         next;
1445                 } elsif ($f =~ /^<(\S+)/) {     # this is bbs syntax  for from call
1446                         $from = $1;
1447                 } elsif ($f =~ /^\$\S+/) {     # this is bbs syntax for bid
1448                         ;
1449                 } else {
1450
1451                         # callsign ?
1452                         $notincalls = 0;
1453
1454                         # is this callsign a distro?
1455                         my $fn = "$msgdir/distro/$f.pl";
1456                         if (-e $fn) {
1457                                 my $fh = new IO::File $fn;
1458                                 if ($fh) {
1459                                         local $/ = undef;
1460                                         my $s = <$fh>;
1461                                         $fh->close;
1462                                         my @call;
1463                                         @call = eval $s;
1464                                         return (1, "Error in Distro $f.pl:", $@) if $@;
1465                                         if (@call > 0) {
1466                                                 push @f, @call;
1467                                                 next;
1468                                         }
1469                                 }
1470                         }
1471                         
1472                         if (grep $_ eq $f, @DXMsg::badmsg) {
1473                                 push @out, $dxchan->msg('m3', $f);
1474                         } else {
1475                                 push @to, $f;
1476                         }
1477                 }
1478         }
1479         
1480         # subject is the next line
1481         my $subject = shift @$ref;
1482         
1483         # strip off trailing lines 
1484         pop @$ref while (@$ref && $$ref[-1] =~ /^\s*$/);
1485         
1486         # strip off /EX or /ABORT
1487         return ("aborted") if @$ref && $$ref[-1] =~ m{^/ABORT$}i; 
1488         pop @$ref if (@$ref && $$ref[-1] =~ m{^/EX$}i);                                                                  
1489
1490         # sort out any splitting that needs to be done
1491         my @chunk;
1492         if ($splitit) {
1493                 my $lth = 0;
1494                 my $lines = [];
1495                 for (@$ref) {
1496                         if ($lth >= $maxchunk || ($lth > $minchunk && /^\s*$/)) {
1497                                 push @chunk, $lines;
1498                                 $lines = [];
1499                                 $lth = 0;
1500                         } 
1501                         push @$lines, $_;
1502                         $lth += length; 
1503                 }
1504                 push @chunk, $lines if @$lines;
1505         } else {
1506                 push @chunk, $ref;
1507         }
1508
1509         # does an identical message already exist?
1510         my $m;
1511         for $m (@msg) {
1512                 if (substr($subject,0,28) eq substr($m->{subject},0,28) && $from eq $m->{from} && grep $m->{to} eq $_, @to) {
1513                         my $msgno = $m->{msgno};
1514                         dbg("duplicate message from $from -> $m->{to} to msg: $msgno") if isdbg('msg');
1515                         Log('msg', "duplicate message from $from -> $m->{to} to msg: $msgno");
1516                         return;
1517                 }
1518         }
1519
1520     # write all the messages away
1521         my $i;
1522         for ( $i = 0;  $i < @chunk; $i++) {
1523                 my $chunk = $chunk[$i];
1524                 my $ch_subject;
1525                 if (@chunk > 1) {
1526                         my $num = " [" . ($i+1) . "/" . scalar @chunk . "]";
1527                         $ch_subject = substr($subject, 0, 27 - length $num) .  $num;
1528                 } else {
1529                         $ch_subject = $subject;
1530                 }
1531                 my $to;
1532                 foreach $to (@to) {
1533                         my $systime = $main::systime;
1534                         my $mycall = $main::mycall;
1535                         my $mref = DXMsg->alloc(DXMsg::next_transno('Msgno'),
1536                                                                         $to,
1537                                                                         $from, 
1538                                                                         $systime,
1539                                                                         $private, 
1540                                                                         $ch_subject, 
1541                                                                         $origin,
1542                                                                         '0',
1543                                                                         $rr);
1544                         $mref->swop_it($main::mycall);
1545                         $mref->store($chunk);
1546                         $mref->add_dir();
1547                         push @out, $dxchan->msg('m11', $mref->{msgno}, $to);
1548                         #push @out, "msgno $ref->{msgno} sent to $to";
1549                         $mref->notify;
1550                 }
1551         }
1552         return @out;
1553 }
1554
1555 #no strict;
1556 sub AUTOLOAD
1557 {
1558         no strict;
1559         my $name = $AUTOLOAD;
1560         return if $name =~ /::DESTROY$/;
1561         $name =~ s/^.*:://o;
1562         
1563         confess "Non-existant field '$AUTOLOAD'" if !$valid{$name};
1564         # this clever line of code creates a subroutine which takes over from autoload
1565         # from OO Perl - Conway
1566         *$AUTOLOAD = sub {@_ > 1 ? $_[0]->{$name} = $_[1] : $_[0]->{$name}};
1567        goto &$AUTOLOAD;
1568 }
1569
1570 1;
1571
1572 __END__