fix bull age deletion both periodically and on new entry
[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 @words;
350                                 for (@{$ref->{lines}}) {
351                                         push @words, BadWords::check($_);
352                                 }
353                                 push @words, BadWords::check($ref->{subject});
354                                 if (@words) {
355                                         dbg("$ref->{from} swore: '@words' -> $ref->{to} '$ref->{subject}' origin: $ref->{origin} via " . $dxchan->call) if isdbg('msg');
356                                         Log('msg',"$ref->{from} swore: '@words' -> $ref->{to} origin: $ref->{origin} via " . $dxchan->call);
357                                         Log('msg',"subject: $ref->{subject}");
358                                         for (@{$ref->{lines}}) {
359                                                 Log('msg', "line: $_");
360                                         }
361                                         $ref->stop_msg($fromnode);
362                                         return;
363                                 }
364                                                         
365                                 $ref->{msgno} = next_transno("Msgno");
366                                 push @{$ref->{gotit}}, $fromnode; # mark this up as being received
367                                 $ref->store($ref->{lines});
368                                 $ref->notify;
369                                 add_dir($ref);
370                                 Log('msg', "Message $ref->{msgno} from $ref->{from} received from $fromnode for $ref->{to}");
371                         }
372                 }
373                 $ref->stop_msg($fromnode);
374         } else {
375                 dbg("PC32 from unknown stream $stream from $fromnode") if isdbg('msg');
376                 $dxchan->send(DXProt::pc42($fromnode, $tonode, $stream));       # unknown stream
377         }
378         # queue_msg(0);
379 }
380                 
381 # acknowledge the end of message
382 sub handle_33
383 {
384         my $dxchan = shift;
385         my ($tonode, $fromnode, $stream) = @_[1..3];
386         
387         my $ref = get_fwq($fromnode, $stream);
388         if ($ref) {
389                 if ($ref->{private}) {  # remove it if it private and gone off site#
390                         Log('msg', "Message $ref->{msgno} from $ref->{from} sent to $fromnode and deleted");
391                         $ref->mark_delete;
392                 } else {
393                         Log('msg', "Message $ref->{msgno} from $ref->{from} sent to $fromnode");
394                         push @{$ref->{gotit}}, $fromnode; # mark this up as being received
395                         $ref->store($ref->{lines});     # re- store the file
396                 }
397                 $ref->stop_msg($fromnode);
398         } else {
399                 dbg("PC33 from unknown stream $stream from $fromnode") if isdbg('msg');
400                 $dxchan->send(DXProt::pc42($fromnode, $tonode, $stream));       # unknown stream
401         } 
402
403         # send next one if present
404         queue_msg(0);
405 }
406                 
407 # this is a file request
408 sub handle_40
409 {
410         my $dxchan = shift;
411         my ($tonode, $fromnode) = @_[1..2];
412         
413         $_[3] =~ s/\\/\//og;            # change the slashes
414         $_[3] =~ s/\.//og;                      # remove dots
415         $_[3] =~ s/^\///o;                      # remove the leading /
416         $_[3] = lc $_[3];                       # to lower case;
417         dbg("incoming file $_[3]\n") if isdbg('msg');
418         $_[3] = 'packclus/' . $_[3] unless $_[3] =~ /^packclus\//o;
419                         
420         # create any directories
421         my @part = split /\//, $_[3];
422         my $part;
423         my $fn = "$main::root";
424         pop @part;                                      # remove last part
425         foreach $part (@part) {
426                 $fn .= "/$part";
427                 next if -e $fn;
428                 last SWITCH if !mkdir $fn, 0777;
429                 dbg("created directory $fn\n") if isdbg('msg');
430         }
431         my $stream = next_transno($fromnode);
432         my $ref = DXMsg->alloc($stream, "$main::root/$_[3]", $dxchan->call, time, !$_[4], $_[3], ' ', '0', '0');
433                         
434         # forwarding variables
435         $ref->{fromnode} = $tonode;
436         $ref->{tonode} = $fromnode;
437         $ref->{linesreq} = $_[5];
438         $ref->{stream} = $stream;
439         $ref->{count} = 0;                      # no of lines between PC31s
440         $ref->{file} = 1;
441         $ref->{lastt} = $main::systime;
442         set_fwq($fromnode, $stream, $ref); # store in work
443         $dxchan->send(DXProt::pc30($fromnode, $tonode, $stream)); # send ack 
444 }
445                 
446 # abort transfer
447 sub handle_42
448 {
449         my $dxchan = shift;
450         my ($tonode, $fromnode, $stream) = @_[1..3];
451         
452         dbg("stream $stream: abort received\n") if isdbg('msg');
453         my $ref = get_fwq($fromnode, $stream);
454         if ($ref) {
455                 $ref->stop_msg($fromnode);
456                 $ref = undef;
457         }
458 }
459
460 # global delete on subject
461 sub handle_49
462 {
463         my $dxchan = shift;
464         my $line = shift;
465         
466         for (@msg) {
467                 if ($_->{from} eq $_[1] && $_->{subject} eq $_[2]) {
468                         $_->mark_delete;
469                         Log('msg', "Message $_->{msgno} from $_->{from} ($_->{subject}) fully deleted");
470                         DXChannel::broadcast_nodes($line, $dxchan);
471                 }
472         }
473 }
474
475
476
477 sub notify
478 {
479         my $ref = shift;
480         my $to = $ref->{to};
481         my $uref = DXUser->get_current($to);
482         my $dxchan = DXChannel->get($to);
483         if (((*Net::SMTP && $email_server) || $email_prog) && $uref && $uref->wantemail) {
484                 my $email = $uref->email;
485                 if ($email) {
486                         my @rcpt = ref $email ? @{$email} : $email;
487                         my $fromaddr = $email_from || $main::myemail;
488                         my @headers = ("To: $ref->{to}", 
489                                                    "From: $fromaddr",
490                                                    "Subject: [DXSpider: $ref->{from}] $ref->{subject}", 
491                                                    "X-DXSpider-To: $ref->{to}",
492                                                    "X-DXSpider-From: $ref->{from}\@$ref->{origin}", 
493                                                    "X-DXSpider-Gateway: $main::mycall"
494                                                   );
495                         my @data = ("Msgno: $ref->{msgno} To: $to From: $ref->{from}\@$ref->{origin} Gateway: $main::mycall", 
496                                                 "", 
497                                                 $ref->read_msg_body
498                                            );
499                         my $msg;
500                         undef $!;
501                         if (*Net::SMTP && $email_server) {
502                                 $msg = Net::SMTP->new($email_server);
503                                 if ($msg) {
504                                         $msg->mail($fromaddr);
505                                         $msg->to(@rcpt);
506                                         $msg->data(map {"$_\n"} @headers, '', @data);
507                                         $msg->quit;
508                                 }
509                         } elsif ($email_prog) {
510                                 $msg = new IO::File "|$email_prog " . join(' ', @rcpt);
511                                 if ($msg) {
512                                         print $msg map {"$_\r\n"} @headers, '', @data, '.';
513                                         $msg->close;
514                                 }
515                         }
516                         dbg("email forwarding error $!") if isdbg('msg') && !$msg && defined $!; 
517                 }
518         }
519         $dxchan->send($dxchan->msg('m9')) if $dxchan && $dxchan->is_user;
520 }
521
522 # store a message away on disc or whatever
523 #
524 # NOTE the second arg is a REFERENCE not a list
525 sub store
526 {
527         my $ref = shift;
528         my $lines = shift;
529
530         if ($ref->{file}) {                     # a file
531                 dbg("To be stored in $ref->{to}\n") if isdbg('msg');
532                 
533                 my $fh = new IO::File "$ref->{to}", "w";
534                 if (defined $fh) {
535                         my $line;
536                         foreach $line (@{$lines}) {
537                                 print $fh "$line\n";
538                         }
539                         $fh->close;
540                         dbg("file $ref->{to} stored\n") if isdbg('msg');
541                         Log('msg', "file $ref->{to} from $ref->{from} stored" );
542                 } else {
543                         confess "can't open file $ref->{to} $!";  
544                 }
545         } else {                                        # a normal message
546
547                 # attempt to open the message file
548                 my $fn = filename($ref->{msgno});
549                 
550                 dbg("To be stored in $fn\n") if isdbg('msg');
551                 
552                 # now save the file, overwriting what's there, YES I KNOW OK! (I will change it if it's a problem)
553                 my $fh = new IO::File "$fn", "w";
554                 if (defined $fh) {
555                         my $rr = $ref->{rrreq} ? '1' : '0';
556                         my $priv = $ref->{private} ? '1': '0';
557                         my $del = $ref->{delete} ? '1' : '0';
558                         my $delt = $ref->{deletetime} || ($ref->{t} + $maxage);
559                         my $keep = $ref->{keep} || '0';
560                         print $fh "=== $ref->{msgno}^$ref->{to}^$ref->{from}^$ref->{t}^$priv^$ref->{subject}^$ref->{origin}^$ref->{'read'}^$rr^$del^$delt^$keep\n";
561                         print $fh "=== ", join('^', @{$ref->{gotit}}), "\n";
562                         my $line;
563                         $ref->{size} = 0;
564                         foreach $line (@{$lines}) {
565                                 $line =~ s/[\x00-\x08\x0a-\x1f\x80-\x9f]/./g;
566                                 $ref->{size} += (length $line) + 1;
567                                 print $fh "$line\n";
568                         }
569                         $fh->close;
570                         dbg("msg $ref->{msgno} stored\n") if isdbg('msg');
571                         Log('msg', "msg $ref->{msgno} from $ref->{from} to $ref->{to} stored" );
572                 } else {
573                         confess "can't open msg file $fn $!";  
574                 }
575         }
576
577 }
578
579 # delete a message
580 sub del_msg
581 {
582         my $self = shift;
583         my $dxchan = shift;
584         my $call = '';
585         $call = ' by ' . $dxchan->call if $dxchan;
586         
587         if ($self->{tonode}) {
588                 $self->{delete}++;
589                 $self->{deletetime} = 0;
590                 dbg("Msgno $self->{msgno} but marked as expunged$call") if isdbg('msg');
591         } else {
592                 # remove it from the active message list
593                 @msg = grep { $_ != $self } @msg;
594
595                 Log('msg', "Msgno $self->{msgno} expunged$call");
596                 dbg("Msgno $self->{msgno} expunged$call") if isdbg('msg');
597                 
598                 # remove the file
599                 unlink filename($self->{msgno});
600         }
601 }
602
603 sub mark_delete
604 {
605         my $ref = shift;
606         my $t = shift;
607
608         return if $ref->{keep};
609         
610         $t = $main::systime + $residencetime unless defined $t;
611         
612         $ref->{delete}++;
613         $ref->{deletetime} = $t;
614         $ref->store( [$ref->read_msg_body] );
615 }
616
617 sub unmark_delete
618 {
619         my $ref = shift;
620         my $t = shift;
621         $ref->{delete} = 0;
622         $ref->{deletetime} = 0;
623 }
624
625 # clean out old messages from the message queue
626 sub clean_old
627 {
628         my $ref;
629         
630         # mark old messages for deletion
631         foreach $ref (@msg) {
632                 if (ref($ref) && !$ref->{keep} && $ref->{deletetime} < $main::systime) {
633
634                         # this is for IMMEDIATE destruction
635                         $ref->{delete}++;
636                         $ref->{deletetime} = 0;
637                 }
638         }
639 }
640
641 # read in a message header
642 sub read_msg_header
643
644         my $fn = shift;
645         my $file;
646         my $line;
647         my $ref;
648         my @f;
649         my $size;
650         
651         $file = new IO::File "$fn";
652         if (!$file) {
653             dbg("Error reading $fn $!");
654             Log('err', "Error reading $fn $!");
655                 return undef;
656         }
657         $size = -s $fn;
658         $line = <$file>;                        # first line
659         if ($size == 0 || !$line) {
660             dbg("Empty $fn $!");
661             Log('err', "Empty $fn $!");
662                 return undef;
663         }
664         chomp $line;
665         $size -= length $line;
666         if (! $line =~ /^===/o) {
667                 dbg("corrupt first line in $fn ($line)");
668                 Log('err', "corrupt first line in $fn ($line)");
669                 return undef;
670         }
671         $line =~ s/^=== //o;
672         @f = split /\^/, $line;
673         $ref = DXMsg->alloc(@f);
674         
675         $line = <$file>;                        # second line
676         chomp $line;
677         $size -= length $line;
678         if (! $line =~ /^===/o) {
679             dbg("corrupt second line in $fn ($line)");
680             Log('err', "corrupt second line in $fn ($line)");
681                 return undef;
682         }
683         $line =~ s/^=== //o;
684         $ref->{gotit} = [];
685         @f = split /\^/, $line;
686         push @{$ref->{gotit}}, @f;
687         $ref->{size} = $size;
688         
689         close($file);
690         
691         return $ref;
692 }
693
694 # read in a message header
695 sub read_msg_body
696 {
697         my $self = shift;
698         my $msgno = $self->{msgno};
699         my $file;
700         my $line;
701         my $fn = filename($msgno);
702         my @out;
703         
704         $file = new IO::File;
705         if (!open($file, $fn)) {
706                 dbg("Error reading $fn $!");
707                 Log('err' ,"Error reading $fn $!");
708                 return ();
709         }
710         @out = map {chomp; $_} <$file>;
711         close($file);
712         
713         shift @out if $out[0] =~ /^=== /;
714         shift @out if $out[0] =~ /^=== /;
715         return @out;
716 }
717
718 # send a tranche of lines to the other end
719 sub send_tranche
720 {
721         my ($self, $dxchan) = @_;
722         my @out;
723         my $to = $self->{tonode};
724         my $from = $self->{fromnode};
725         my $stream = $self->{stream};
726         my $lines = $self->{lines};
727         my ($c, $i);
728         
729         for ($i = 0, $c = $self->{count}; $i < $self->{linesreq} && $c < @$lines; $i++, $c++) {
730                 push @out, DXProt::pc29($to, $from, $stream, $lines->[$c]);
731     }
732     $self->{count} = $c;
733
734     push @out, DXProt::pc32($to, $from, $stream) if $i < $self->{linesreq};
735         $dxchan->send(@out);
736 }
737
738         
739 # find a message to send out and start the ball rolling
740 sub queue_msg
741 {
742         my $sort = shift;
743         my $ref;
744         my $clref;
745         
746         # bat down the message list looking for one that needs to go off site and whose
747         # nearest node is not busy.
748
749         dbg("queue msg ($sort)\n") if isdbg('msg');
750         my @nodelist = DXChannel::get_all_nodes;
751         foreach $ref (@msg) {
752
753                 # ignore 'delayed' messages until their waiting time has expired
754                 if (exists $ref->{waitt}) {
755                         next if $ref->{waitt} > $main::systime;
756                         delete $ref->{waitt};
757                 } 
758
759                 # any time outs?
760                 if (exists $ref->{lastt} && $main::systime >= $ref->{lastt} + $timeout) {
761                         my $node = $ref->{tonode};
762                         dbg("Timeout, stopping msgno: $ref->{msgno} -> $node") if isdbg('msg');
763                         Log('msg', "Timeout, stopping msgno: $ref->{msgno} -> $node");
764                         $ref->stop_msg($node);
765                         
766                         # delay any outgoing messages that fail
767                         $ref->{waitt} = $main::systime + $waittime + int rand(120) if $node ne $main::mycall;
768                         delete $ref->{lastt};
769                         next;
770                 }
771
772                 # is it being sent anywhere currently?
773                 next if $ref->{tonode};           # ignore it if it already being processed
774                 
775                 # is it awaiting deletion?
776                 next if $ref->{delete};
777                 
778                 # firstly, is it private and unread? if so can I find the recipient
779                 # in my cluster node list offsite?
780
781                 # deal with routed private messages
782                 my $dxchan;
783                 if ($ref->{private}) {
784                         next if $ref->{'read'};           # if it is read, it is stuck here
785                         $clref = Route::get($ref->{to});
786                         if ($clref) {
787                                 $dxchan = $clref->dxchan;
788                                 if ($dxchan) {
789                                         if ($dxchan->is_node) {
790                                                 next if $clref->call eq $main::mycall;  # i.e. it lives here
791                                                 $ref->start_msg($dxchan) if !get_busy($dxchan->call)  && $dxchan->state eq 'normal';
792                                         }
793                                 } else {
794                                         dbg("Route: No dxchan for $ref->{to} " . ref($clref) ) if isdbg('msg');
795                                 }
796                         }
797                 } else {
798                         
799                         # otherwise we are dealing with a bulletin or forwarded private message
800                         # compare the gotit list with
801                         # the nodelist up above, if there are sites that haven't got it yet
802                         # then start sending it - what happens when we get loops is anyone's
803                         # guess, use (to, from, time, subject) tuple?
804                         foreach $dxchan (@nodelist) {
805                                 my $call = $dxchan->call;
806                                 next unless $call;
807                                 next if $call eq $main::mycall;
808                                 next if ref $ref->{gotit} && grep $_ eq $call, @{$ref->{gotit}};
809                                 next unless $ref->forward_it($call);           # check the forwarding file
810                                 next if $ref->{tonode};           # ignore it if it already being processed
811                                 
812                                 # if we are here we have a node that doesn't have this message
813                                 if (!get_busy($call)  && $dxchan->state eq 'normal') {
814                                         $ref->start_msg($dxchan);
815                                         last;
816                                 }
817                         }
818                 }
819
820                 # if all the available nodes are busy then stop
821                 last if @nodelist == scalar grep { get_busy($_->call) } @nodelist;
822         }
823
824         
825 }
826
827 # is there a message for me?
828 sub for_me
829 {
830         my $call = uc shift;
831         my $ref;
832         my $count;
833         
834         foreach $ref (@msg) {
835                 # is it for me, private and unread? 
836                 if ($ref->{to} eq $call && $ref->{private}) {
837                    $count++ unless $ref->{'read'} || $ref->{delete};
838                 }
839         }
840         return $count;
841 }
842
843 # start the message off on its travels with a PC28
844 sub start_msg
845 {
846         my ($self, $dxchan) = @_;
847         
848         confess("trying to start started msg $self->{msgno} nodes: $self->{fromnode} -> $self->{tonode}") if $self->{tonode};
849         dbg("start msg $self->{msgno}\n") if isdbg('msg');
850         $self->{linesreq} = 10;
851         $self->{count} = 0;
852         $self->{tonode} = $dxchan->call;
853         $self->{fromnode} = $main::mycall;
854         set_busy($self->{tonode}, $self);
855         set_fwq($self->{tonode}, undef, $self);
856         $self->{lastt} = $main::systime;
857         my ($fromnode, $origin);
858         $fromnode = $self->{fromnode};
859         $origin = $self->{origin};
860         $dxchan->send(DXProt::pc28($self->{tonode}, $fromnode, $self->{to}, $self->{from}, $self->{t}, $self->{private}, $self->{subject}, $origin, $self->{rrreq}));
861 }
862
863 # get the ref of a busy node
864 sub get_busy
865 {
866         my $call = shift;
867         return $busy{$call};
868 }
869
870 sub set_busy
871 {
872         my $call = shift;
873         return $busy{$call} = shift;
874 }
875
876 sub del_busy
877 {
878         my $call = shift;
879         return delete $busy{$call};
880 }
881
882 # get the whole busy queue
883 sub get_all_busy
884 {
885         return keys %busy;
886 }
887
888 # get a forwarding queue entry
889 sub get_fwq
890 {
891         my $call = shift;
892         my $stream = shift || '0';
893         return $work{"$call,$stream"};
894 }
895
896 # delete a forwarding queue entry
897 sub del_fwq
898 {
899         my $call = shift;
900         my $stream = shift || '0';
901         return delete $work{"$call,$stream"};
902 }
903
904 # set a fwq entry
905 sub set_fwq
906 {
907         my $call = shift;
908         my $stream = shift || '0';
909         return $work{"$call,$stream"} = shift;
910 }
911
912 # get the whole forwarding queue
913 sub get_all_fwq
914 {
915         return keys %work;
916 }
917
918 # stop a message from continuing, clean it out, unlock interlocks etc
919 sub stop_msg
920 {
921         my $self = shift;
922         my $node = shift;
923         my $stream = $self->{stream};
924         
925         
926         dbg("stop msg $self->{msgno} -> node $node\n") if isdbg('msg');
927         del_fwq($node, $stream);
928         $self->workclean;
929         del_busy($node);
930 }
931
932 sub workclean
933 {
934         my $ref = shift;
935         delete $ref->{lines};
936         delete $ref->{linesreq};
937         delete $ref->{tonode};
938         delete $ref->{fromnode};
939         delete $ref->{stream};
940         delete $ref->{file};
941         delete $ref->{count};
942         delete $ref->{tempr};
943         delete $ref->{lastt};
944         delete $ref->{waitt};
945 }
946
947 # get a new transaction number from the file specified
948 sub next_transno
949 {
950         my $name = shift;
951         $name =~ s/\W//og;                      # remove non-word characters
952         my $fn = "$msgdir/$name";
953         my $msgno;
954         
955         my $fh = new IO::File;
956         if (sysopen($fh, $fn, O_RDWR|O_CREAT, 0666)) {
957                 $fh->autoflush(1);
958                 $msgno = $fh->getline || '0';
959                 chomp $msgno;
960                 $msgno++;
961                 seek $fh, 0, 0;
962                 $fh->print("$msgno\n");
963                 dbg("msgno $msgno allocated for $name\n") if isdbg('msg');
964                 $fh->close;
965         } else {
966                 confess "can't open $fn $!";
967         }
968         return $msgno;
969 }
970
971 # initialise the message 'system', read in all the message headers
972 sub init
973 {
974         my $dir = new IO::File;
975         my @dir;
976         my $ref;
977                 
978         # load various control files
979         dbg("load badmsg: " . (load_badmsg() or "Ok"));
980         dbg("load forward: " . (load_forward() or "Ok"));
981         dbg("load swop: " . (load_swop() or "Ok"));
982
983         # read in the directory
984         opendir($dir, $msgdir) or confess "can't open $msgdir $!";
985         @dir = readdir($dir);
986         closedir($dir);
987
988         @msg = ();
989         for (sort @dir) {
990                 next unless /^m\d\d\d\d\d\d$/;
991                 
992                 $ref = read_msg_header("$msgdir/$_");
993                 unless ($ref) {
994                         dbg("Deleting $_");
995                         Log('err', "Deleting $_");
996                         unlink "$msgdir/$_";
997                         next;
998                 }
999                 
1000                 # delete any messages to 'badmsg.pl' places
1001                 if ($ref->dump_it('')) {
1002                         dbg("'Bad' TO address $ref->{to}") if isdbg('msg');
1003                         Log('msg', "'Bad' TO address $ref->{to}");
1004                         $ref->del_msg;
1005                         next;
1006                 }
1007
1008                 # add the message to the available queue
1009                 add_dir($ref); 
1010         }
1011 }
1012
1013 # add the message to the directory listing
1014 sub add_dir
1015 {
1016         my $ref = shift;
1017         confess "tried to add a non-ref to the msg directory" if !ref $ref;
1018         push @msg, $ref;
1019 }
1020
1021 # return all the current messages
1022 sub get_all
1023 {
1024         return @msg;
1025 }
1026
1027 # get a particular message
1028 sub get
1029 {
1030         my $msgno = shift;
1031         for (@msg) {
1032                 return $_ if $_->{msgno} == $msgno;
1033                 last if $_->{msgno} > $msgno;
1034         }
1035         return undef;
1036 }
1037
1038 # return the official filename for a message no
1039 sub filename
1040 {
1041         return sprintf "$msgdir/m%06d", shift;
1042 }
1043
1044 #
1045 # return a list of valid elements 
1046
1047
1048 sub fields
1049 {
1050         return keys(%valid);
1051 }
1052
1053 #
1054 # return a prompt for a field
1055 #
1056
1057 sub field_prompt
1058
1059         my ($self, $ele) = @_;
1060         return $valid{$ele};
1061 }
1062
1063 #
1064 # send a message state machine
1065 sub do_send_stuff
1066 {
1067         my $self = shift;
1068         my $line = shift;
1069         my @out;
1070         
1071         if ($self->state eq 'send1') {
1072                 #  $DB::single = 1;
1073                 confess "local var gone missing" if !ref $self->{loc};
1074                 my $loc = $self->{loc};
1075                 if (my @ans = BadWords::check($line)) {
1076                         $self->{badcount} += @ans;
1077                         Log('msg', $self->call . " used badwords: @ans to @{$loc->{to}} in msg");
1078                         $loc->{reject}++;
1079                 }
1080                 $loc->{subject} = $line;
1081                 $loc->{lines} = [];
1082                 $self->state('sendbody');
1083                 #push @out, $self->msg('sendbody');
1084                 push @out, $self->msg('m8');
1085         } elsif ($self->state eq 'sendbody') {
1086                 confess "local var gone missing" if !ref $self->{loc};
1087                 my $loc = $self->{loc};
1088                 if ($line eq "\032" || $line eq '%1A' || uc $line eq "/EX") {
1089                         my $to;
1090                         unless ($loc->{reject}) {
1091                                 foreach $to (@{$loc->{to}}) {
1092                                         my $ref;
1093                                         my $systime = $main::systime;
1094                                         my $mycall = $main::mycall;
1095                                         $ref = DXMsg->alloc(DXMsg::next_transno('Msgno'),
1096                                                                                 uc $to,
1097                                                                                 exists $loc->{from} ? $loc->{from} : $self->call, 
1098                                                                                 $systime,
1099                                                                                 $loc->{private}, 
1100                                                                                 $loc->{subject}, 
1101                                                                                 exists $loc->{origin} ? $loc->{origin} : $mycall,
1102                                                                                 '0',
1103                                                                                 $loc->{rrreq});
1104                                         $ref->swop_it($self->call);
1105                                         $ref->store($loc->{lines});
1106                                         $ref->add_dir();
1107                                         push @out, $self->msg('m11', $ref->{msgno}, $to);
1108                                         #push @out, "msgno $ref->{msgno} sent to $to";
1109                                         $ref->notify;
1110                                 }
1111                         } else {
1112                                 Log('msg', $self->call . " swore to @{$loc->{to}} subject: '$loc->{subject}' in msg, REJECTED");
1113                         }
1114                         
1115                         delete $loc->{lines};
1116                         delete $loc->{to};
1117                         delete $self->{loc};
1118                         $self->func(undef);
1119                         
1120                         $self->state('prompt');
1121                 } elsif ($line eq "\031" || uc $line eq "/ABORT" || uc $line eq "/QUIT") {
1122                         #push @out, $self->msg('sendabort');
1123                         push @out, $self->msg('m10');
1124                         delete $loc->{lines};
1125                         delete $loc->{to};
1126                         delete $self->{loc};
1127                         $self->func(undef);
1128                         $self->state('prompt');
1129                 } elsif ($line =~ m|^/+\w+|) {
1130                         # this is a command that you want display for your own reference
1131                         # or if it has TWO slashes is a command 
1132                         $line =~ s|^/||;
1133                         my $store = $line =~ s|^/+||;
1134                         my @in = $self->run_cmd($line);
1135                         push @out, @in;
1136                         if ($store) {
1137                                 foreach my $l (@in) {
1138                                         if (my @ans = BadWords::check($l)) {
1139                                                 $self->{badcount} += @ans;
1140                                                 Log('msg', $self->call . " used badwords: @ans to @{$loc->{to}} subject: '$loc->{subject}' in msg") unless $loc->{reject};
1141                                                 Log('msg', "line: $l");
1142                                                 $loc->{reject}++;
1143                                         } 
1144                                         push @{$loc->{lines}}, length($l) > 0 ? $l : " ";
1145                                 }
1146                         }
1147                 } else {
1148                         if (my @ans = BadWords::check($line)) {
1149                                 $self->{badcount} += @ans;
1150                                 Log('msg', $self->call . " used badwords: @ans to @{$loc->{to}} subject: '$loc->{subject}' in msg") unless $loc->{reject};
1151                                 Log('msg', "line: $line");
1152                                 $loc->{reject}++;
1153                         }
1154
1155                         if ($loc->{lines} && @{$loc->{lines}}) {
1156                                 push @{$loc->{lines}}, length($line) > 0 ? $line : " ";
1157                         } else {
1158                                 # temporarily store any R: lines so that we end up with 
1159                                 # only the first and last ones stored.
1160                                 if ($line =~ m|^R:\d{6}/\d{4}|) {
1161                                         push @{$loc->{tempr}}, $line;
1162                                 } else {
1163                                         if (exists $loc->{tempr}) {
1164                                                 push @{$loc->{lines}}, shift @{$loc->{tempr}};
1165                                                 push @{$loc->{lines}}, pop @{$loc->{tempr}} if @{$loc->{tempr}};
1166                                                 delete $loc->{tempr};
1167                                         }
1168                                         push @{$loc->{lines}}, length($line) > 0 ? $line : " ";
1169                                 } 
1170                         }
1171                         
1172                         # i.e. it ain't and end or abort, therefore store the line
1173                 }
1174         }
1175         return @out;
1176 }
1177
1178 # return the standard directory line for this ref 
1179 sub dir
1180 {
1181         my $ref = shift;
1182         my $flag = $ref->{private} && $ref->{read} ? '-' : ' ';
1183         if ($ref->{keep}) {
1184                 $flag = '!';
1185         } elsif ($ref->{delete}) {
1186                 $flag = $ref->{deletetime} > $main::systime ? 'D' : 'E'; 
1187         }
1188         return sprintf("%6d%s%s%5d %8.8s %8.8s %-6.6s %5.5s %-30.30s", 
1189                                    $ref->{msgno}, $flag, $ref->{private} ? 'p' : ' ', 
1190                                    $ref->{size}, $ref->{to}, $ref->{from}, cldate($ref->{t}), 
1191                                    ztime($ref->{t}), $ref->{subject});
1192 }
1193
1194 # load the forward table
1195 sub load_forward
1196 {
1197         my @out;
1198         my $s = readfilestr($forwardfn);
1199         if ($s) {
1200                 eval $s;
1201                 push @out, $@ if $@;
1202         }
1203         return @out;
1204 }
1205
1206 # load the bad message table
1207 sub load_badmsg
1208 {
1209         my @out;
1210         my $s = readfilestr($badmsgfn);
1211         if ($s) {
1212                 eval $s;
1213                 push @out, $@ if $@;
1214         }
1215         return @out;
1216 }
1217
1218 # load the swop message table
1219 sub load_swop
1220 {
1221         my @out;
1222         my $s = readfilestr($swopfn);
1223         if ($s) {
1224                 eval $s;
1225                 push @out, $@ if $@;
1226         }
1227         return @out;
1228 }
1229
1230 #
1231 # forward that message or not according to the forwarding table
1232 # returns 1 for forward, 0 - to ignore
1233 #
1234
1235 sub forward_it
1236 {
1237         my $ref = shift;
1238         my $call = shift;
1239         my $i;
1240         
1241         for ($i = 0; $i < @forward; $i += 5) {
1242                 my ($sort, $field, $pattern, $action, $bbs) = @forward[$i..($i+4)]; 
1243                 my $tested;
1244                 
1245                 # are we interested?
1246                 next if $ref->{private} && $sort ne 'P';
1247                 next if !$ref->{private} && $sort ne 'B';
1248                 
1249                 # select field
1250                 $tested = $ref->{to} if $field eq 'T';
1251                 $tested = $ref->{from} if $field eq 'F';
1252                 $tested = $ref->{origin} if $field eq 'O';
1253                 $tested = $ref->{subject} if $field eq 'S';
1254
1255                 if (!$pattern || $tested =~ m{$pattern}i) {
1256                         return 0 if $action eq 'I';
1257                         return 1 if !$bbs || grep $_ eq $call, @{$bbs};
1258                 }
1259         }
1260         return 0;
1261 }
1262
1263 #
1264 # look down the forward table to see whether this is a valid bull
1265 # or not (ie it will forward somewhere even if it is only here)
1266 #
1267 sub valid_bull_addr
1268 {
1269         my $call = shift;
1270         my $i;
1271         
1272         unless (@forward) {
1273                 return 1 if $call =~ /^ALL/;
1274                 return 1 if $call =~ /^DX/;
1275                 return 0;
1276         }
1277         
1278         for ($i = 0; $i < @forward; $i += 5) {
1279                 my ($sort, $field, $pattern, $action, $bbs) = @forward[$i..($i+4)]; 
1280                 if ($field eq 'T') {
1281                         if (!$pattern || $call =~ m{$pattern}i) {
1282                                 return 1;
1283                         }
1284                 }
1285         }
1286         return 0;
1287 }
1288
1289 sub dump_it
1290 {
1291         my $ref = shift;
1292         my $call = shift;
1293         my $i;
1294         
1295         for ($i = 0; $i < @badmsg; $i += 3) {
1296                 my ($sort, $field, $pattern) = @badmsg[$i..($i+2)]; 
1297                 my $tested;
1298                 
1299                 # are we interested?
1300                 next if $ref->{private} && $sort ne 'P';
1301                 next if !$ref->{private} && $sort ne 'B';
1302                 
1303                 # select field
1304                 $tested = $ref->{to} if $field eq 'T';
1305                 $tested = $ref->{from} if $field eq 'F';
1306                 $tested = $ref->{origin} if $field eq 'O';
1307                 $tested = $ref->{subject} if $field eq 'S';
1308                 $tested = $call if $field eq 'I';
1309
1310                 if (!$pattern || $tested =~ m{$pattern}i) {
1311                         return 1;
1312                 }
1313         }
1314         return 0;
1315 }
1316
1317 sub swop_it
1318 {
1319         my $ref = shift;
1320         my $call = shift;
1321         my $i;
1322         my $count = 0;
1323         
1324         for ($i = 0; $i < @swop; $i += 5) {
1325                 my ($sort, $field, $pattern, $tfield, $topattern) = @swop[$i..($i+4)]; 
1326                 my $tested;
1327                 my $swop;
1328                 my $old;
1329                 
1330                 # are we interested?
1331                 next if $ref->{private} && $sort ne 'P';
1332                 next if !$ref->{private} && $sort ne 'B';
1333                 
1334                 # select field
1335                 $tested = $ref->{to} if $field eq 'T';
1336                 $tested = $ref->{from} if $field eq 'F';
1337                 $tested = $ref->{origin} if $field eq 'O';
1338                 $tested = $ref->{subject} if $field eq 'S';
1339
1340                 # select swop field
1341                 $old = $swop = $ref->{to} if $tfield eq 'T';
1342                 $old = $swop = $ref->{from} if $tfield eq 'F';
1343                 $old = $swop = $ref->{origin} if $tfield eq 'O';
1344                 $old = $swop = $ref->{subject} if $tfield eq 'S';
1345
1346                 if ($tested =~ m{$pattern}i) {
1347                         if ($tested eq $swop) {
1348                                 $swop =~ s{$pattern}{$topattern}i;
1349                         } else {
1350                                 $swop = $topattern;
1351                         }
1352                         Log('msg', "Msg $ref->{msgno}: $tfield $old -> $swop");
1353                         Log('dbg', "Msg $ref->{msgno}: $tfield $old -> $swop");
1354                         $ref->{to} = $swop if $tfield eq 'T';
1355                         $ref->{from} = $swop if $tfield eq 'F';
1356                         $ref->{origin} = $swop if $tfield eq 'O';
1357                         $ref->{subject} = $swop if $tfield eq 'S';
1358                         ++$count;
1359                 }
1360         }
1361         return $count;
1362 }
1363
1364 # import any msgs in the import directory
1365 # the messages are in BBS format (but may have cluster extentions
1366 # so SB UK < GB7TLH is legal
1367 sub import_msgs
1368 {
1369         # are there any to do in this directory?
1370         return unless -d $importfn;
1371         unless (opendir(DIR, $importfn)) {
1372                 dbg("can\'t open $importfn $!") if isdbg('msg');
1373                 Log('msg', "can\'t open $importfn $!");
1374                 return;
1375         } 
1376
1377         my @names = readdir(DIR);
1378         closedir(DIR);
1379         my $name;
1380         foreach $name (@names) {
1381                 next if $name =~ /^\./;
1382                 my $splitit = $name =~ /^split/;
1383                 my $fn = "$importfn/$name";
1384                 next unless -f $fn;
1385                 unless (open(MSG, $fn)) {
1386                         dbg("can\'t open import file $fn $!") if isdbg('msg');
1387                         Log('msg', "can\'t open import file $fn $!");
1388                         unlink($fn);
1389                         next;
1390                 }
1391                 my @msg = map { chomp; $_ } <MSG>;
1392                 close(MSG);
1393                 unlink($fn);
1394                 my @out = import_one($main::me, \@msg, $splitit);
1395                 Log('msg', @out);
1396         }
1397 }
1398
1399 # import one message as a list in bbs (as extended) mode
1400 # takes a reference to an array containing the whole message
1401 sub import_one
1402 {
1403         my $dxchan = shift;
1404         my $ref = shift;
1405         my $splitit = shift;
1406         my $private = '1';
1407         my $rr = '0';
1408         my $notincalls = 1;
1409         my $from = $dxchan->call;
1410         my $origin = $main::mycall;
1411         my @to;
1412         my @out;
1413                                 
1414         # first line;
1415         my $line = shift @$ref;
1416         my @f = split /([\s\@\$])/, $line;
1417         @f = map {s/\s+//g; length $_ ? $_ : ()} @f;
1418
1419         unless (@f && $f[0] =~ /^(:?S|SP|SB|SEND)$/ ) {
1420                 my $m = "invalid first line in import '$line'";
1421                 dbg($m) if isdbg('msg');
1422                 return (1, $m);
1423         }
1424         while (@f) {
1425                 my $f = uc shift @f;
1426                 next if $f eq 'SEND';
1427
1428                 # private / noprivate / rr
1429                 if ($notincalls && ($f eq 'B' || $f eq 'SB' || $f =~ /^NOP/oi)) {
1430                         $private = '0';
1431                 } elsif ($notincalls && ($f eq 'P' || $f eq 'SP' || $f =~ /^PRI/oi)) {
1432                         ;
1433                 } elsif ($notincalls && ($f eq 'RR')) {
1434                         $rr = '1';
1435                 } elsif (($f =~ /^[\@\.\#\$]$/ || $f eq '.#') && @f) {       # this is bbs syntax, for AT
1436                         shift @f;
1437                 } elsif ($f eq '<' && @f) {     # this is bbs syntax  for from call
1438                         $from = uc shift @f;
1439                 } elsif ($f =~ /^\$/) {     # this is bbs syntax  for a bid
1440                         next;
1441                 } elsif ($f =~ /^<(\S+)/) {     # this is bbs syntax  for from call
1442                         $from = $1;
1443                 } elsif ($f =~ /^\$\S+/) {     # this is bbs syntax for bid
1444                         ;
1445                 } else {
1446
1447                         # callsign ?
1448                         $notincalls = 0;
1449
1450                         # is this callsign a distro?
1451                         my $fn = "$msgdir/distro/$f.pl";
1452                         if (-e $fn) {
1453                                 my $fh = new IO::File $fn;
1454                                 if ($fh) {
1455                                         local $/ = undef;
1456                                         my $s = <$fh>;
1457                                         $fh->close;
1458                                         my @call;
1459                                         @call = eval $s;
1460                                         return (1, "Error in Distro $f.pl:", $@) if $@;
1461                                         if (@call > 0) {
1462                                                 push @f, @call;
1463                                                 next;
1464                                         }
1465                                 }
1466                         }
1467                         
1468                         if (grep $_ eq $f, @DXMsg::badmsg) {
1469                                 push @out, $dxchan->msg('m3', $f);
1470                         } else {
1471                                 push @to, $f;
1472                         }
1473                 }
1474         }
1475         
1476         # subject is the next line
1477         my $subject = shift @$ref;
1478         
1479         # strip off trailing lines 
1480         pop @$ref while (@$ref && $$ref[-1] =~ /^\s*$/);
1481         
1482         # strip off /EX or /ABORT
1483         return ("aborted") if @$ref && $$ref[-1] =~ m{^/ABORT$}i; 
1484         pop @$ref if (@$ref && $$ref[-1] =~ m{^/EX$}i);                                                                  
1485
1486         # sort out any splitting that needs to be done
1487         my @chunk;
1488         if ($splitit) {
1489                 my $lth = 0;
1490                 my $lines = [];
1491                 for (@$ref) {
1492                         if ($lth >= $maxchunk || ($lth > $minchunk && /^\s*$/)) {
1493                                 push @chunk, $lines;
1494                                 $lines = [];
1495                                 $lth = 0;
1496                         } 
1497                         push @$lines, $_;
1498                         $lth += length; 
1499                 }
1500                 push @chunk, $lines if @$lines;
1501         } else {
1502                 push @chunk, $ref;
1503         }
1504
1505         # does an identical message already exist?
1506         my $m;
1507         for $m (@msg) {
1508                 if (substr($subject,0,28) eq substr($m->{subject},0,28) && $from eq $m->{from} && grep $m->{to} eq $_, @to) {
1509                         my $msgno = $m->{msgno};
1510                         dbg("duplicate message from $from -> $m->{to} to msg: $msgno") if isdbg('msg');
1511                         Log('msg', "duplicate message from $from -> $m->{to} to msg: $msgno");
1512                         return;
1513                 }
1514         }
1515
1516     # write all the messages away
1517         my $i;
1518         for ( $i = 0;  $i < @chunk; $i++) {
1519                 my $chunk = $chunk[$i];
1520                 my $ch_subject;
1521                 if (@chunk > 1) {
1522                         my $num = " [" . ($i+1) . "/" . scalar @chunk . "]";
1523                         $ch_subject = substr($subject, 0, 27 - length $num) .  $num;
1524                 } else {
1525                         $ch_subject = $subject;
1526                 }
1527                 my $to;
1528                 foreach $to (@to) {
1529                         my $systime = $main::systime;
1530                         my $mycall = $main::mycall;
1531                         my $mref = DXMsg->alloc(DXMsg::next_transno('Msgno'),
1532                                                                         $to,
1533                                                                         $from, 
1534                                                                         $systime,
1535                                                                         $private, 
1536                                                                         $ch_subject, 
1537                                                                         $origin,
1538                                                                         '0',
1539                                                                         $rr);
1540                         $mref->swop_it($main::mycall);
1541                         $mref->store($chunk);
1542                         $mref->add_dir();
1543                         push @out, $dxchan->msg('m11', $mref->{msgno}, $to);
1544                         #push @out, "msgno $ref->{msgno} sent to $to";
1545                         $mref->notify;
1546                 }
1547         }
1548         return @out;
1549 }
1550
1551 #no strict;
1552 sub AUTOLOAD
1553 {
1554         no strict;
1555         my $name = $AUTOLOAD;
1556         return if $name =~ /::DESTROY$/;
1557         $name =~ s/^.*:://o;
1558         
1559         confess "Non-existant field '$AUTOLOAD'" if !$valid{$name};
1560         # this clever line of code creates a subroutine which takes over from autoload
1561         # from OO Perl - Conway
1562         *$AUTOLOAD = sub {@_ > 1 ? $_[0]->{$name} = $_[1] : $_[0]->{$name}};
1563        goto &$AUTOLOAD;
1564 }
1565
1566 1;
1567
1568 __END__