package FTN::Pkt;

use strict;
use warnings;
require 5.6.0;
our $VERSION = "1.02";

package FTN::Pkt::utils;

use strict;
use warnings;
require Exporter;
use vars qw(@ISA @EXPORT_OK %EXPORT_TAGS);

BEGIN
{
    @ISA = qw(Exporter);
    %EXPORT_TAGS = (utils => [qw(parse_addr datetime trunk trunkzero hextime my_sleep)]);
    Exporter::export_ok_tags('utils');
    1;
}

use POSIX qw(strftime);
use Time::HiRes qw(usleep gettimeofday);

my $PRECISION = 0.1;

#========================================================

# Here is some auxiliary functions. Not for client use.

#========================================================

sub parse_addr($)
{
    my $addr = shift;
    return (undef, undef, undef, undef) unless $addr;
    $addr .= ".0" unless $addr =~ /\.\d+$/;
    my @result = $addr =~ /(\d)\:(\d+)\/(\d+)\.(\d+)/;
    return @result;
}

#========================================================

sub datetime(;$)
{
    my $tm = shift;
    $tm ||= time();
    my @MON = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
    my @curtime = localtime($tm);
    return strftime("%d ", @curtime).$MON[$curtime[4]].strftime(" %y  %H:%M:%S", @curtime);
}

#========================================================

sub trunk($$)
{
    my ($str, $len) = @_;
    if (length($str) > $len && $len > 0){
        $str = substr($str, 0, $len);
    }
    return $str;
}

#========================================================

sub trunkzero($$)
{
    return trunk($_[0], $_[1]) . "\0";
}

#========================================================

sub hextime()
{
    my $msec = int(gettimeofday() / $PRECISION) % 0xffffffff;
    return sprintf("%08x", $msec);
}

#========================================================

sub my_sleep()
{
    usleep($PRECISION*1000000*1.1);
}

#========================================================

package FTN::Msg;

use strict;
use warnings;

import FTN::Pkt::utils qw(:utils);


#========================================================

use fields qw(fromaddr toaddr fromname toname tearline origin subj text area msgid reply
              topkt frompkt pid tid date);

#========================================================

sub new
{
    my FTN::Msg $self = shift;
    $self = fields::new($self) unless ref $self;
    $self->update(@_);
    return $self;
}

#========================================================

sub update
{
    my FTN::Msg $self = shift;
    my %params = @_;
    foreach(keys %params){
        $self->{$_} = $params{$_};
    }
}

#========================================================

sub make_msgid(;$)
{
    my FTN::Msg $self = shift;
    my $msgid = shift;
    unless ($msgid){
        $msgid = hextime();
        my_sleep();
    }
    die "make_msgid: unknown fromaddr" unless $self->{fromaddr};
    return ($self->{msgid} = "$self->{fromaddr} $msgid");
}

#========================================================

sub as_string()
{
    my FTN::Msg $self = shift;
    my $res = "\n";
    foreach(qw (fromname fromaddr toname toaddr frompkt topkt frompkt area msgid reply pid subj)) {
        $res .= "$_ : $self->{$_}\n" if exists $self->{$_} and defined $self->{$_};
    }
    $res .= '-' x 72 . "\n$self->{text}\n".'-' x 72 ."\n"
        if exists $self->{text} and defined $self->{text};
    foreach(qw (tearline origin)) {
        $res .= "$_ : $self->{$_}\n" if exists $self->{$_} and defined $self->{$_};
    }
    return $res;
}

#========================================================
#
# Internal method. Returns binary representation of the message inside the packet.

sub _packed()
{
    my FTN::Msg $self = shift;
    my ($fromzone, $fromnet, $fromnode, $frompoint) = parse_addr($self->{fromaddr});
    my ($tozone, $tonet, $tonode, $topoint) = parse_addr($self->{toaddr});
    my ($pfromzone, $pfromnet, $pfromnode, $pfrompoint) = parse_addr($self->{frompkt});
    my ($ptozone, $ptonet, $ptonode, $ptopoint) = parse_addr($self->{topkt});
    my $template = "v7a20";
    $self->make_msgid() unless ($self->{msgid});
    my $result = pack $template, 2, $pfromnode, $ptonode, $pfromnet, $ptonet,
                      0, 0, datetime($self->{date});
    $result .= trunkzero(($self->{toname} ? $self->{toname} : "All"), 35);
    $result .= trunkzero($self->{fromname}, 35);
    $result .= trunkzero(($self->{subj} ? $self->{subj} : ""), 71);
    my $msgtail = "\x0";
    if ($self->{area}){
        $result .= "AREA:".$self->{area}."\xd";
        $msgtail = "SEEN-BY: $pfromnet/$pfromnode\x0d\x01PATH: $pfromnet/$pfromnode\x0d\x00";
# -------------->
    }else{
        $result .= "\x01INTL $tozone:$tonet/$tonode $fromzone:$fromnet/$fromnode\xd";
        $result .= "\x01FMPT $frompoint\xd" if $frompoint != 0;
        $result .= "\x01TOPT $topoint\xd" if $topoint != 0;
    }
    $result .= "\x01REPLY: $self->{reply}\x0d" if $self->{reply};
    $result .= "\x01MSGID: $self->{msgid}\x0d";
    $result .= "\x01CHRS: CP866 2\x0d";
    $result .= "\x01PID: $self->{pid}\x0d" if $self->{pid};
    $result .= sprintf("\x01TID: FTN::Pkt %s\x0d", $FTN::Pkt::VERSION) if $self->{tid}; 
    $result .= "\x01Posted: ".datetime()."\x0d" if $self->{date};
    my $text = $self->{text};
    $text =~ s/\n/\xd/sg;
    $result .= $text;
    $result .= "\x0d--- ".($self->{tearline} ? $self->{tearline} : "")."\xd";
    my $origin = " * Origin: ";
    my $origtext = ($self->{origin} ? $self->{origin} : "");
    my $origtail = " (".$self->{fromaddr}.")\xd";
    my $origtxln = 79 - length ($origin.$origtail);
    $origtext = trunk($origtext, $origtxln);
    $origin .= $origtext .= $origtail;
    $result .= $origin if ($self->{origin} || $self->{area});
    $result .= $msgtail;
    return $result;
}

#========================================================

package FTN::Pkt;
use strict;
use warnings;

import FTN::Pkt::utils qw(:utils);

use FTN::Utils::OS_features;
use Carp qw(croak);


#========================================================

use fields qw(fromaddr toaddr password inbound _msgs);

#========================================================

# fromaddr, toaddr, password, inbound
# msgs

sub new {

    my FTN::Pkt $self = shift;
    $self = fields::new($self) unless ref $self;
    $self->update(@_);
    $self->{_msgs} = [];
    return $self;
}

#========================================================

sub update
{
    my FTN::Pkt $self = shift;
    my %params = @_;
    if(exists $params{_msgs}){
        croak "FATAL: can't update '_msgs' directly!";
    }
    foreach(keys %params){
        $self->{$_} = $params{$_};
    }
}

#========================================================

sub add_msg($)
{
    my FTN::Pkt $self = shift;
    my $msg = shift;
    push @{$self->{_msgs}}, $msg;
}

#========================================================
#
# Internal method. Returns binary representation of the packet.

sub _packed()
{
    my FTN::Pkt $self = shift;
    my ($fromzone, $fromnet, $fromnode, $frompoint) = parse_addr($self->{fromaddr});
    my ($tozone, $tonet, $tonode, $topoint) = parse_addr($self->{toaddr});
    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);

    my $template = "v13a8v10V";
    my $result = pack $template, $fromnode, $tonode,
                      $year+1900, $mon, $mday, $hour, $min, $sec, 0, 2,
                      $fromnet, $tonet, 0x7766,
                      $self->{password} ? $self->{password} : "",
                      $fromzone, $tozone, 0, 0x100, 0x7766, 1,
                      $fromzone, $tozone, $frompoint, $topoint, 0;
    foreach my $msg(@{$self->{_msgs}}){
        $msg->update(frompkt => $self->{fromaddr}, topkt => $self->{toaddr});
        $result .= $msg->_packed();
    }
    $result .= "\x00\x00";
    return $result;
}


#========================================================

sub write_pkt()
{
    my FTN::Pkt $self = shift;
    my $regexp = "${dir_separator}\$";
    $self->{inbound} .= $dir_separator unless $self->{inbound} =~ /$regexp/;
    my $filename = $self->{inbound}.hextime() .".tmp";
    my $newname = $filename;
    $newname =~ s/tmp$/pkt/;
    my @repl = split //, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
    for(my $i = 0; -e $filename; $i++){
        if($i >= scalar @repl){die "can't make unique tmp name";}
        substr($filename, -12, 1) = $repl[$i];
    }
    open(PKT, ">", $filename) or die "can't open $filename : $!";
    binmode PKT if $needs_binmode;
    print PKT $self->_packed();
    close PKT;
    for(my $i = 0; -e $newname; $i++){
        if($i >= scalar @repl){die "can't make unique pkt name";}
        substr($newname, -12, 1) = $repl[$i];
    }
    rename $filename, $newname or die "can't rename $filename -> $newname : $!";
    return $newname;
}

#========================================================

1;

=head1 NAME

FTN::Pkt - a module to make FTN-style mail packets

=head1 SYNOPSIS
    
    my $pkt = new FTN::Pkt (
        fromaddr => '2:9999/999.128',
        toaddr   => '2:9999/999',
        password => 'password',
        inbound  => '/var/spool/fido/inbound'
    );    
    my $msg = new FTN::Msg(
        fromname => 'Vassily Poupkine',
        toname   => 'Poupa Vassilyev',
        subj     => 'Hello',
        text     => "Hi, Poupa!\n\nHow do you do?\n\n--\nVassily",
        fromaddr => '2:9999/999.128',
        origin   => 'My cool origin',
        tearline => '/usr/bin/perl',
        area     => 'poupa.local',
        reply    => '2:9999/999.1 fedcba987',
        date     => 1210918822,                    # unixtime format
        pid      => 'Super-Duper Editor v0.01',
        tid      => 1
    );
    $pkt->add_msg($msg);    
    $pkt->write_pkt();

=head1 DESCRIPTION

This module can be used to make FTN-style mail packets. Either echomail or netmail are supported.
You can specify @REPLY cludge. @MSGID may be auto-generated or specified manually.

If C<area> present then message treated as echomail. Othervise it becomes netmail (C<toaddr> required).

=head1 FTN::Msg methods

=over 8

=item C<new(%hash)>

A constructor. Some initialization parameters can be passed via C<%hash>. 
Possible ones are: 
C<fromaddr toaddr fromname toname tearline origin subj text area msgid reply pid tid date>.

All parameters are text but C<tid> is boolean. If I<true> then @TID cludge will be added to message.

=item C<update(%hash)>

Changes the message. See C<FTN::Msg::new> for parameters allowed.

=item C<make_msgid([$msgid])>

Generates @MSGID, sets it inside the message and and returns it. Possible parameter is only second part of @MSGID, without source address.
If parameter omitted then all @MSGID parts will be auto-generated. Auto-generation method use I<unixtime> as basis, 
so don't allow more than one process to generate @MSGIDs in the same time.

=item C<as_string()>

Returns string representation of message. For debugging.

=back

=head1 FTN::Pkt methods

=over 8

=item C<new(%hash)>

A constructor. Some initialization parameters can be passed via C<%hash>. 
Possible ones are: C<fromaddr toaddr password inbound>

=item C<update(%hash)>

Changes the packet. See C<FTN::Pkt::new> for parameters allowed.

=item C<add_msg($msg)>

Adds a message to the packet. C<$msg> must be a reference to C<FTN::Msg> object.

=item C<write_pkt()>

Writes the packet to a disk into C<inbound> directory. Filename is auto-generated. 
Don't allow more than one process to write at the same time. Returns resulting filename.

=back

=head1 LIMITATIONS

CP866 codepage is hardcoded.

=head1 REQUIREMENTS

FTN::OS_features module required (included in this package). 

Supported platforms: UNIX and Win32 have been tested. All others may work or not.

=head1 COPYRIGHT

Copyright 2008 Dmitry V. Kolvakh

This library is free software. 
You may copy or redistribute it under the same terms as Perl itself.

=head1 AUTHOR

Dmitry V. Kolvakh aka Keu

2:5054/89@FIDOnet

=cut