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