#!/usr/bin/perl -w
#############################################################################
# License:
#
# This software (hereafter referred to as "program") is free software;
#   you can redistribute it and/or modify it under the terms of the GNU General
#   Public License as published by the Free Software Foundation; either version
#   2 of the License, or (at your option) any later version.
# Note that when redistributing modified versions of this source code, you
#   must ensure that this disclaimer and the above coder's names are included
#   VERBATIM in the modified code.
#
# Disclaimer:
# This program is provided with no warranty of any kind, either expressed or
#   implied.  It is the responsibility of the user (you) to fully research and
#   comprehend the usage of this program.  As with any tool, it can be misused,
#   either intentionally (you're a vandal) or unintentionally (you're a moron).
#   THE AUTHOR(S) IS(ARE) NOT RESPONSIBLE FOR ANYTHING YOU DO WITH THIS PROGRAM
#   or anything that happens because of your use (or misuse) of this program,
#   including but not limited to anything you, your lawyers, or anyone else
#   can dream up.  And now, a relevant quote directly from the GPL:
#
#                           NO WARRANTY
#
#  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
# FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
# OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
# PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
# OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
# TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
# PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
# REPAIR OR CORRECTION.
#
#   ---
#
# Whee, that was fun, wasn't it?  Now let's all get together and think happy
#   thoughts - and remember, the same class of people who made all the above
#   legal spaghetti necessary are the same ones who are stripping your rights
#   away with DVD CCA, DMCA, stupid software patents, and retarded legal
#   challenges to everything we've come to hold dear about our internet.
#   So enjoy your dwindling "freedom" while you can, because 1984 is coming
#   sooner than you think.  :[
#
#############################################################################
# 
# Purpose:
#     To create a program to send pages to a SNPP RFC-1861 compliant server.
# 
# 
# Features:
#       - Easy addition of new service providers by simply editing a hash
#       - Logging to a file
#       - Script returns error status to the system when an error occurs
#       - Verbose error messages to STDOUT when problems occur
#       - Multiple debugging levels
#       - Automatic message truncation when message exceeds providers limit
# 
# 
# Technical Description:
#     Does a standard TCP connect to the providers SNPP service, and sends
#     the message in RFC 1861 compliant manner.
#     
# 
# 
# Changelog:
#     08/29/2002 - v1.0-pre
#         - Script Creation
#
#     08/30/2002 - v1.0-beta1
#         - Added many more providers
#         - Changed listProviders() to sort it's output
#         - Re-wrote the connectTo() function to be compliant with my new
#           standards for function return codes. It now returns 0 on success.
#         - Took out some providers that don't seem to resolve
#
#     10/08/2002 - v1.0-RC1
#         - Added support for paging to multiple devices at once
# 
#     01/31/2003 - v1.00
#         - Version RC1 has been stable and in production for several
#           months, so it is being released as v1.0.
#         - Minor textual changes
#     
#     02/04/2003 - v1.01
#         - Textual changes to a many error messages
#
#     03/28/2003 - v1.02
#         - During SNPP chat, change PAGER command to PAGE.  According to
#           RFC 1861 PAGE is more correct.  This was causing issues with
#           WeblinkWireless2Way as reported by John Olson-Kennedy.
#     
#     11/08/2005 - v1.03
#         - Updated the Verizon SNPP server, thanks to Chris Nowlin for
#           the updated info.
# 
#############################################################################
use strict;
use IO::Socket qw (:DEFAULT :crlf);


## Global Variable(s)
my %conf = (
    "programName"          => $0,                                ## Program name and version
    "version"              => "1.03",
    "authorName"           => 'Brandon Zehm <caspian@dotconf.net> and Steve Kaylor',      ## Information about the author or owner of this script
    "authorEmail"          => 'steversk@steversk.homeip.net',
    "debug"                => 0,
    "error"                => "",                                ## Used for storing error messages
    
    ## These values get used later in the program
    "provider"             => "",
    "message"              => "",
    "receiver"             => "",
    "logFile"              => "",
    "logging"              => "",
);
$conf{"programName"} =~ s/(.)*[\/,\\]//;                         ## Remove path from programName





## Provider hash - edit this to add new providers
## Note that this list IS case sensitive.
## FYI this list came from http://www.notepager.net/snpp.htm
my %provider = (

##  Provider Name                     Hostname                     Port    Max-message-length  ##
    
    "AdvancedPagingWireless"     => [ "gateway.adv-supr.com",      444,    230 ],
    "Airtouch"                   => [ "snpp.airtouch.com",         444,    240 ],
    "AllCom"                     => [ "207.2.221.125",             444,    245 ],
    "Ameritch"                   => [ "pagemart.com",              444,      0 ],
    "AzCom"                      => [ "aszom.net",                 444,      0 ],
    "Baystar"                    => [ "209.44.230.3",              444,      0 ],
    "BellMobility"               => [ "pmcl.net",                  444,      0 ],    
    "CommunicationsSpecialists"  => [ "pageme.comspeco.com",       444,      0 ],
    "ContactPaging"              => [ "207.140.31.163",            444,      0 ],
    "CTC"                        => [ "snpp.metrocall.com",        444,      0 ],
    "DataComm"                   => [ "209.191.211.38",            444,      0 ],
    "DataPage"                   => [ "63.166.125.242",            444,      0 ],
    "DigiPage"                   => [ "page.pageks.com",           444,      0 ],
    "ExtelMobile"                => [ "extel-gsm.com",             444,      0 ],
    "Graylink"                   => [ "epage.graylink.com",        444,      0 ],
    "Highland"                   => [ "alphapage.cc",              444,    250 ],
    "LauttamusCommunications"    => [ "avlc1.lauttamus.com",       444,    250 ],
    "Metrocall"                  => [ "snpp.metrocall.com",        444,    200 ],
    "NetworkServices"            => [ "12.29.171.249",             444,      0 ],
    "Nextel"                     => [ "pecos.nextel.com",          444,      0 ],
    "Nextel2way"                 => [ "snpp.nextel.com",           444,      0 ],
    "NortheastPaging"            => [ "epage.graylink.com",        444,    230 ],
    "OmnicomPaging"              => [ "mail.pagerpeople.com",      444,      0 ],
    "PagemartCanada"             => [ "pmcl.net",                  444,      0 ],
    "PriorityCommunication"      => [ "mail2.sendalpha.com",       444,      0 ],
    "ProPage"                    => [ "page.propage.net",          444,    240 ],
    "Porta-phonePaging"          => [ "epage.porta-phone.com",     444,    150 ],
    "Satellink"                  => [ "snpp.satellink.net",        444,    240 ],
    "Skytel"                     => [ "snpp.skytel.com",           444,    240 ],
    "Verizon"                    => [ "snpp.myairmail.com",        444,      0 ],    
    "WeblinkWireless"            => [ "pagemart.net",              444,      0 ],    
    "wood" 	 	         => [ "68.119.96.39",              444,      0 ],    
    "WeblinkWireless2Way"        => [ "airmessage.net",            444,      0 ],    

## The folowing services appear to be dead, or we have incorrect information
#    "AAA"                        => [ "wirelessgate.snpp.net",     444,      0 ],
#    "Alphanow"                   => [ "alphanow.net",              444,      0 ],  
#    "Infopage"                   => [ "snpp.infopagesystems.com",  444,      0 ],
#    "Tele-Trak"                  => [ "page.tele-track.com",       444,      0 ],
#    "TSCNet"                     => [ "kes.tscnet.com",            444,      0 ],
#    "UCP"                        => [ "alpahnow.net",              444,    120 ],    

);









#############################
##
##      MAIN PROGRAM
##
#############################


## Process Command Line
processCommandLine();


## Open the log file if needed
if ($conf{'logFile'}) {
    if (openLogFile($conf{'logFile'})) {
        quit("ERROR: An error occured while opening the log file [$conf{'logFile'}].  The error was: $!", 1);
    }
}


## Connect to the correct SNPP server
if (connectTo($provider{$conf{'provider'}}[0], $provider{$conf{'provider'}}[1])) { 
    quit($conf{'error'}, 1);
}


## Send the page
if (SNPPchat($conf{'receiver'}, $conf{'message'})) {
    quit($conf{'error'}, 1);
}


## Disconnect from the server
close SERVER;


## Quit with a nice exit message
quit("SUCCESS: The message was sent successfully to $conf{'receiver'} through $conf{'provider'}.",0);
































######################################################################
## Function:    help ()
##
## Description: For all those newbies ;) 
##              Prints a help message and exits the program.
## 
######################################################################
sub help {
print <<EOM;

$conf{'programName'}-$conf{'version'} by $conf{'authorName'} <$conf{'authorEmail'}>

Usage:  $conf{'programName'} [options]
  
  Required:
    -p <provider>             to see a list of providers try "-h provider"
    -m <message>              message - this is usually limited to 240 characters
    -r <receiver>             usually a pager number (use multiple times for more
                              than one pager - not all SNPP servers support this)
    
  Optional:
    -l <logfile>              enable logging to a file
    -h [provider]             or --help[=provider]
    -v                        verbosity - use multiple times for greater effect

EOM
quit("", 1);
}










######################################################################
## Function:    listProviders ()
##
## Description: Displays the list of providers from the global
##              variable %provider
## 
######################################################################
sub listProviders {
    print "\n$conf{'programName'}-$conf{'version'} by $conf{'authorName'} <$conf{'authorEmail'}>\n\n";
    print "Supported Providers:\n";
    foreach my $key (sort(keys(%provider))) {
        print "    $key\n";
    }
    print "\n";
    return(1);
}










######################################################################
##  Function: initialize ()
##  
##  Does all the script startup jibberish.
##  
######################################################################
sub initialize {

  ## Set STDOUT to flush immediatly after each print  
  $| = 1;

  ## Intercept signals
  $SIG{'QUIT'}  = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
  $SIG{'INT'}   = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
  $SIG{'KILL'}  = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
  $SIG{'TERM'}  = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
  
  ## ALARM and HUP signals are not supported in Win32
  unless ($^O =~ /win/i) {
    $SIG{'HUP'}   = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
    $SIG{'ALRM'}  = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
  }
  
  return(1);
}













######################################################################
##  Function: processCommandLine ()
##  
##  Processes command line storing important data in global var %conf
##  
######################################################################
sub processCommandLine {
  
  
  ############################
  ##  Process command line  ##
  ############################
  
  my $numargv = scalar(@ARGV);
  help() unless ($numargv);
  for (my $counter = 0; $counter < $numargv; $counter++) {
      
      if ($ARGV[$counter] =~ /^-h$|^--help/i) {           ## Help ##
          if ($ARGV[$counter] =~ /provider/i) { 
              listProviders();
              quit("", 1);
          }
          if ( ($ARGV[$counter + 1]) and ($ARGV[$counter + 1] =~ /provider/i) ) {
              listProviders();
              quit("", 1);
          }
          help();
      }
      
      elsif ($ARGV[$counter] =~ s/^-v+//i) {               ## Verbosity ##
          my $tmp = (length($&) - 1);
          $conf{'debug'} += $tmp;
      }
      
      elsif ($ARGV[$counter] =~ /^-p$/i) {                 ## Provider ##
          $counter++;
          $conf{'provider'} = $ARGV[$counter];
      }
      
      elsif ($ARGV[$counter] =~ /^-m$/i) {                 ## Message ##
          $counter++;
          $conf{'message'} = $ARGV[$counter];
      }
      
      elsif ($ARGV[$counter] =~ /^-r$/i) {                 ## Receiver ##
          $counter++;
          $conf{'receiver'} .= "$ARGV[$counter] ";
      }
      
      elsif ($ARGV[$counter] =~ /^-l$/i) {                 ## Log File ##
          $counter++;
          $conf{'logFile'} = $ARGV[$counter];
      }
      
      else {                                               ## Invalid Option ##
          quit("ERROR:  The option '$ARGV[$counter]' is unrecognised.", 1);
      }

  }
  
  ###############################################
  ##  Debugging - Print the contents of %conf  ##
  ###############################################
  
  printmsg ("Configuration: ", 4); foreach my $key (keys(%conf)) { printmsg ("Key: $key =>\t Value: $conf{$key}", 4) if ($key and $conf{$key}); }
  
    
  ###################################################
  ##  Verify required variables are set correctly  ##
  ###################################################
  my @required = (
                    'provider',
                    'message',
                    'receiver',
  );
  foreach (@required) {
      if (!$conf{$_}) {
          quit("ERROR: Value [$_] was not defined.  Try $conf{'programName'} -h", 1);
      }
  }
  
  ## Make sure the reciever value contains only numeric values.
  if ($conf{'receiver'} !~ /^(\d|\s)+$/) {
      quit("ERROR: The receiver value must not contain non-numeric values.", 1);
  }
  
  ## Make sure the provider is valid
  if (!$provider{$conf{'provider'}}[0]) {
      quit("ERROR: The provider [$conf{'provider'}] is not known.  Try \"$conf{'programName'} -h provider\" for a list of providers.", 1);
  }
  
  ## Check message length - if it's too long truncate it and print a warning message
  if ( ($provider{$conf{'provider'}}[2] != 0) and (length($conf{'message'}) > $provider{$conf{'provider'}}[2]) ) {
      printmsg("WARNING: The message your sending will be truncated because it is longer than the limit of $provider{$conf{'provider'}}[2] characters.", 0);
      $conf{'message'} = substr($conf{'message'}, 0, $provider{$conf{'provider'}}[2]);
  }
  
  ## Make sure the message does not have any line ending characters in it.
  $conf{'message'} =~ s/\r\n|\n\r|\r|\n/ /ig;
  
  return(1);
}













###############################################################################################
##  Function:    printmsg (string $message, int $level)
##
##  Description: Handles all messages - logging them to a log file, 
##               printing them to the screen or both depending on
##               the $level passed in, $conf{'debug'} and wether
##               $conf{'mode'}.
##
##  Input:       $message          A message to be printed, logged, etc.
##               $level            The debug level of the message. If
##                                 not defined 0 will be assumed.  0 is
##                                 considered a normal message, 1 and 
##                                 higher is considered a debug message.
##  
##  Output:      Prints to STDOUT, to LOGFILE, both, or none depending 
##               on the state of the program.
##  
##  Example:     ("WARNING: We believe in generic error messages... NOT!\n", 1);
###############################################################################################
sub printmsg {
  my %incoming = ();
  (
    $incoming{'message'},
    $incoming{'level'}
  ) = @_;
  $incoming{'level'} = 0 if (!defined($incoming{'level'}));
  
  ## Print to the log file if $conf{'logging'} is true and $conf{'debug'} >= $incoming{'level'}
  if ($conf{'logging'} and ($conf{'debug'} >= $incoming{'level'}) ) {
      print LOGFILE "$$ - " . localtime() . " - $incoming{'message'}\n";
  }
  
  ## Print to STDOUT always
  if ($conf{'debug'} >= $incoming{'level'}) {
      print "$$ - " . localtime() . " - $incoming{'message'}\n";
  }
  
  ## Return
  return(1);
}













###############################################################################################
## FUNCTION:    
##   openLogFile ( $filename )
## 
## 
## DESCRIPTION: 
##   Opens the file $filename and attaches it to the filehandle "LOGFILE".  Returns 0 on success
##   and non-zero on failure.  Error codes are listed below, and the error message gets set in
##   global variable $!.
##   
##   
## Example: 
##   openFile ("/var/log/scanAlert.log");
##
###############################################################################################
sub openLogFile {
    ## Get the incoming filename
    my $filename = $_[0];
    
    ## Make sure our file exists, and if the file doesn't exist then create it
    if ( ! -f $filename ) {
        printmsg("NOTICE: The file [$filename] does not exist.  Creating it now with mode [0600].", 0);
        open (LOGFILE, ">>$filename");
        close LOGFILE;
        chmod (0600, $filename);
    }
    
    ## Now open the file and attach it to a filehandle
    open (LOGFILE,">>$filename") or return (1);
    
    ## Put the file into non-buffering mode
    select LOGFILE;
    $| = 1;
    select STDOUT;
    
    ## Tell the rest of the program that we can log now
    $conf{'logging'} = "yes";
    
    ## Return success
    return(0);
}












###############################################################################################
## FUNCTION: connectTo($server, $port)
##
##
## DESCRIPTION: 
##   Does a TCP/IP network connect to $server:$port and attaches that connection handle to
##   the socket SOCKET.   If the port is left blank port 80 is assumed.  Returns zero on
##   success and non-zero on failure.  If an error occurs the corrosponding error message
##   will get stored in $conf{'error'}
##   
##   
## EXAMPLE: 
##   connectTo ("www.google.com", 80);
##           
###############################################################################################
sub connectTo {
  ## Get incoming variables
  my %incoming = ();
  ( 
    $incoming{'server'},
    $incoming{'port'}
  ) = @_;
  
  printmsg("connectTo() - function entry", 3);
  
  
  ## Check incoming variables
  $incoming{'port'} = 80 if ($incoming{'port'} eq "");
  if ($incoming{'server'} eq "") {
      $conf{'error'} = "ERROR: connectTo() Incoming \$server variable was empty.";
      return(1);
  }
  
  
  ## Open a IP socket in stream mode with tcp protocol
  printmsg("connectTo() - requesting a streaming tcp/ip socket from the system", 3);
  socket(SERVER, PF_INET, SOCK_STREAM, getprotobyname('tcp')) || block {
      $conf{'error'} = "ERROR:  Problem opening a tcp/ip socket with the system.";
      return(2);
  };
  
  
  ## Translating the hostname (or IP) into a socket address structure
  printmsg("connectTo() - translating the hostname (or IP) into a socket address structure", 3);
  my $inet_aton = inet_aton($incoming{'server'}) || block {
      $conf{'error'} = "ERROR: DNS resolution failed for [$incoming{'server'}]";
      return(3);
  };
  
  
  ## Create the data structure $dest by calling sockaddr_in(port, 32bit IP)
  printmsg("connectTo() - packing the port and socket address structure into a single data structure", 3);
  my $dest = sockaddr_in ($incoming{'port'}, $inet_aton) || block {
      $conf{'error'} = "ERROR: Calling sockaddr_in() returned the error: $!";
      return(4);
  };
  
  
  ## Connect our socket to SERVER
  printmsg("connectTo() - connecting the socket to the server", 3);
  connect(SERVER, $dest) || block {
      $conf{'error'} = "ERROR:  Connection attempt to [$incoming{'server'}:$incoming{'port'}] failed! $!";
      return(5);
  };
  printmsg("connectTo() - successfully connected to $incoming{'server'}:$incoming{'port'}", 3);
  
  
  ## Force our output to flush immediatly after a print.
  printmsg("connectTo() - setting non-buffering mode on the network connection", 3);
  select(SERVER);
  $| = 1;
  select(STDOUT);
  
  
  ## Return no errors
  printmsg("connectTo() - sub exit: returning a 0 error-level", 3);
  return(0);
}















###############################################################################################
##  Function:    SNPPchat (string $receiver, string $message )
##
##  Description: Communicates with SNPP server and sends message to 
##               specified pager
##
##  Input:       $receiver         Pager number to send message to.  This may be more than one
##                                 pager number separated by whitespace.  Not all servers 
##                                 support this.
##               $message          Message that will be sent to the pager
##                                
##  
##  Output:      Returns zero on success, or non-zero on error.  
##               Error messages will be stored in $conf{'error'}
##               
##  
##  Example:     ();
###############################################################################################
sub SNPPchat {
    my %incoming = ();
    (
        $incoming{'receiver'},
        $incoming{'message'},
    ) = @_;
    
    
    ## Make sure the system greeted us correctly
    my $status = <SERVER>;
    ## If the result isn't correct, return an error code and message
    if ($status !~ /220/i) {
        $status =~ s/$CRLF//;
        $conf{'error'} = "The system didn't greet us properly!  The server returned [$status]";
        return(1);
    }
    
    
    ## Send the pager number and make sure it accepts it.
    ## I've seen lots of strange errors be returned from this, like "Mailbox Full" and "Unknown Pager ID".
    foreach my $receiver (split(/\s+/, $incoming{'receiver'})) {
        print SERVER "PAGE $receiver$CRLF";
        $status = <SERVER>;
        ## If the result isn't correct, return an error code and message
        if ($status !~ /250/i) {
            $status =~ s/$CRLF//;
            $conf{'error'} = "The Pager ID [$receiver] was rejected with the error [$status].  The pending message was [$incoming{'message'}].";
            return(2);
        }
    }
    
    
    ## Enter message to be sent
    print SERVER "MESS $incoming{'message'}$CRLF";
    $status = <SERVER>;
    ## If the result isn't correct, return an error code and message
    if ($status !~ /250/i) {
        $status =~ s/$CRLF//;
        $conf{'error'} = "Error requesting permission to send a message.  The server returned [$status]";
        return(3);
    }
    
    
    ##  Send the actual message
    print SERVER "SEND$CRLF";
    $status = <SERVER>;
    ## If the result isn't correct, return an error code and message
    if ($status !~ /250/i) {
        $status =~ s/$CRLF//;
        $conf{'error'} = "The message was rejected with the error [$status]";
        return(4);
    }
    
    
    ##  Quit to terminate the connection
    print SERVER "QUIT$CRLF";
    $status = <SERVER>;
    ## If the result isn't correct, return an error code and message
    if ($status !~ /221/i) {
        $status =~ s/$CRLF//;
        $conf{'error'} = "The QUIT command failed with the error [$status]";
        return(5);
    }
    
    
    ## Return Success
    return(0);
}












#####################################################################
##  Function:    quit (string $message, int $errorLevel)
##  
##  Description: Exits the program, optionally printing $message.  It 
##               returns an exit error level of $errorLevel to the 
##               system  (0 means no errors, and is assumed if empty.)
##
##  Example:     quit("Exiting program normally", 0);
######################################################################
sub quit {
  my %incoming = ();
  (
    $incoming{'message'},
    $incoming{'errorLevel'}
  ) = @_;
  $incoming{'errorLevel'} = 0 if (!defined($incoming{'errorLevel'}));
  
  
  ## Print exit message
  if ($incoming{'message'}) { 
    printmsg("$incoming{'message'}", 0);
  }
  
  ## Exit the program
  exit($incoming{'errorLevel'});
}

    
