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