#!/usr/bin/perl -w

######################################################################
###
###   IMPORTANT!
###   Please read the warranty and legal notice 
###   at the end of this file!
###
######################################################################

require 5.000;
use lib '/usr/local/bin',"$ENV{HOME}/bin",'/usr/stud/loescher/bin';
use lib 'd:/bin','c:/mydos','c:/bin';
use slutil;
use English;
use File::Copy;
use Net::NNTP;

######################################################################
### Voreinstellungen
######################################################################

$version = '1.0';
$appname = 'fetchnews';

$newsserver  = '';
$root        = '';
$newsrc      = '';
$logfile     = '';
$logLevel    = '';
$output      = '';
$compression = '';
$timestamp   = '';
$user        = '';
$password    = '';
$method      = '';
$newgrouparticles = 100000;
%newsgroups  = ();

&Hilfe if $ARGV[0];
ReadConfigFile();

$useropt     = "-U $user"     if $user     ne '';
$passwordopt = "-P $password" if $password ne '';

# Exitcodes beim Beenden
$NormalExitCode = 0;
$ErrorExitCode  = 1;

######################################################################
### Unterprogramm-Funktionen für interne Zwecke
######################################################################

sub debug;      # Ausgabe von Informationen
sub warning;    # Ausgabe von Warnungen
sub error;      # Ausgabe von Fehlern
sub myexit;     # Exit und Aufräumen
sub loginit;    # Schreibt einen kleinen Header ins LOG-file
sub logdie;     # Schreibt einen Text ins LOG-file und stirbt dann
sub logprint;   # Schreibt einen Text ins LOG-file

######################################################################
### Log-File und Signal-Handler
######################################################################

# LOG bereits hier starten, denn Konfigurationsfehler sind entscheidend!
open(LOG, ">>$logfile") || die "Kann Logfile '$logfile' nicht schreiben!\n";
select(LOG); $|=1; select(STDOUT); # Buffer ausschalten
loginit;
logprint "Log-Level: $logLevel\n";
debug "WORKINGDIR       = $root\n";
debug "NEWSRC           = $newsrc\n";
debug "OUTPUT           = $output\n";
debug "COMPRESSION      = $compression\n";
debug "NEWSRC           = $newsrc\n";
debug "METHOD           = $method\n";
debug "USER             = $user\n";
debug "PASSWORD         = ", $password ne '' ? '********' : '' , "\n";
debug "BATCHFILE_WITH_TIMESTAMP = $timestamp\n";
debug "NEWGROUPARTICLES = $newgrouparticles\n";
debug "NEWSGROUPS       =\n",join("\n",keys %newsgroups),"\n";

# Signal-Handler installieren
$SIG{HUP}  = \&catch_signal;
$SIG{INT}  = \&catch_signal;
$SIG{QUIT} = \&catch_signal;
$SIG{ABRT} = \&catch_signal;
$SIG{TERM} = \&catch_signal;
$SIG{'__WARN__'} = \&catch_warning;

######################################################################
### Hauptprogramm
######################################################################

&Hilfe if defined $ARGV[0];

# Arbeitsverzeichnis erstellen
unless (-d $root)
{
  mkdir $root, 0755 || die "Kann Verzeichnis '$root' nicht erstellen!\n";
}

GetNewsWithSuckAndCompressOnTheFly() if $method eq 'suckthroughperl';
GetNewsWithSuck()                    if $method eq 'suck';
GetNews()                            if $method eq 'perl';

myexit;

######################################################################
### Unterprogramme
######################################################################

sub GetNews
{
  # News mit Perls NNTP-Modul holen
  # Parameter: -
  # Return: -

#$newsrc = new News::Newsrc;
#$ok = $newsrc->load;
#$set = Set::IntSpan->new($newsrc->get_articles('de.comp.os.linux.misc'));
#while( defined ($num = $set->next) )
#{
#  print "$num\n";
#}

  ErstelleSuckNewsrc();

###
### FIXME: Net::NNTP unterstützt noch kein USER und PASS.
###

  my %newsrcInhalt = ();
  my ($gruppe, $nummer);
  my $fh = FileHandle->new();
  if (-e "$root${slash}sucknewsrc")
  {
    open($fh, "$root${slash}sucknewsrc") ||
    die "Kann '$root${slash}sucknewsrc' nicht zum Lesen öffnen!\n";
    while(<$fh>)
    {
      ($gruppe, $nummer) = split;
      $newsrcInhalt{$gruppe} = $nummer;
    }
    close $fh;
  }

  # News Agent erzeugen
  my $newsagent = Net::NNTP->new($newsserver);

  # Ich bin ein News-Client
  $newsagent->reader();

  chdir $root;
  my $out = FileHandle->new();

  my $date; $date = $timestamp ? "-".longdate() : '';

  # Ausgaben in eine Datei
  if ($output eq 'file')
  {
    if ($compression eq 'none')
    { open($out, "> batch$date"); }
    if ($compression eq 'zip')
    { open($out, "| zip batch$date -"); }
    if ($compression eq 'gzip')
    { open($out, "| gzip -9 > batch$date.gz"); }
    if ($compression eq 'bzip2')
    { open($out, "| bzip2 -9 > batch$date.bz2"); }
  }
  else
  # Ausgaben nach STDOUT
  {
    if ($compression eq 'none')
    { open($out, ">-"); }
    if ($compression eq 'zip')
    { open($out, "| zip - -"); }
    if ($compression eq 'gzip')
    { open($out, "| gzip -9"); }
    if ($compression eq 'bzip2')
    { open($out, "| bzip2 -9"); }
  }

  select(STDERR); $|=1; select(STDOUT); # Buffer ausschalten

  foreach $gruppe (keys %newsrcInhalt)
  {
    print STDERR "\r$gruppe\n";
    my $start = $newsrcInhalt{$gruppe};

    # Welche Artikel gibt es?
    my ($total, $first, $last, $name, $artikel);
    ($total, $first, $last, $name) = $newsagent->group($gruppe);

    # Wenn fetchnews schon einmal gelaufen ist, dann stehen in der newsrc
    # nur positive Zahlen
    if ($start > 0)
    {
      $first = $start;
    }

    # Wenn eine Gruppe zum ersten Mal abgeholt wird, dann stehen in $start eine
    # negative Zahl, die anzeigt, wieviele maximal beim ersten Mal geholt
    # werden sollen.
    if ($start < 0)
    {
      $last = min($first+($start*(-1)), $last);
    }

    $artikel = $newsagent->article($first);
    print $out "#! rnews ", length(join('',@$artikel)), "\n";
    print $out @$artikel;

    for ($i=$first+1; $i<=$last; $i++)
    {
      print STDERR $last-$i, "     \r";
      $newsagent->next();
      $artikel = $newsagent->article();
      print $out "#! rnews ", length(join('',@$artikel)), "\n";
      print $out @$artikel;
    }

    $newsrcInhalt{$gruppe} = $last;
  }
  print STDERR "\n";
  $newsagent->quit();
  close $out;

  # Neue fetchnews.newsrc schreiben
  open($fh, ">$newsrc") || die "Kann '$newsrc' nicht zum Schreiben öffnen!\n";
  foreach (keys %newsrcInhalt)
  {
    print $fh "$_ $newsrcInhalt{$_}\n";
  }
  close $fh;
}


sub GetNewsWithSuckAndCompressOnTheFly
{
  # Holt die News mittels "suck".
  # Parameter: -
  # Return:    -
  #

  ErstelleSuckNewsrc();

  chdir $root;
  my $fh  = FileHandle->new();
  my $out = FileHandle->new();

  my $date; $date = $timestamp ? "-".longdate() : '';

  # Ausgaben in eine Datei
  if ($output eq 'file')
  {
    if ($compression eq 'none')
    { open($out, "> batch$date"); }
    if ($compression eq 'zip')
    { open($out, "| zip batch$date -"); }
    if ($compression eq 'gzip')
    { open($out, "| gzip -9 > batch$date.gz"); }
    if ($compression eq 'bzip2')
    { open($out, "| bzip2 -9 > batch$date.bz2"); }
  }
  else
  # Ausgaben nach STDOUT
  {
    if ($compression eq 'none')
    { open($out, ">-"); }
    if ($compression eq 'zip')
    { open($out, "| zip - -"); }
    if ($compression eq 'gzip')
    { open($out, "| gzip -9"); }
    if ($compression eq 'bzip2')
    { open($out, "| bzip2 -9"); }
  }

  $out->autoflush(1);
  open($fh, "suck $newsserver $useropt $passwordopt --mode_reader --killfile --cleanup |");
  {
    local $/ = "\n.\n"; # Ein einzelner Punkt in einer Zeile ist das Ende
    while (<$fh>)
    {
      # Die von NNTP escaped-en Punkte wiederherstellen
      $_ =~ s/\n\.\./\n./g;
      $len = length $_;
      # rnews-Batches-Größenangaben sind um 2 kürzer, als der Artikel. Warum?
      print $out "#! rnews ",$len-2,"\n";
      print $out substr($_,0,$len-2);
    }
  }
  close $fh;
  close $out;

  # Die neue newsrc wieder zurückkopieren
  copy("$root${slash}sucknewsrc", $newsrc);
}


sub GetNewsWithSuck
{
  # Holt die News mittels "suck".
  # Parameter: -
  # Return:    -
  #

  ErstelleSuckNewsrc();

  if ($output eq 'stdout')
  {
  print STDERR "Option 'OUTPUT STDOUT' ist mit 'METHOD SUCK' nicht möglich!\n";
  }

  chdir $root;
  my $fh = FileHandle->new();
  open($fh, "suck $newsserver $useropt $passwordopt --use_gui --mode_reader --batch_rnews batch ".
       "--killfile --cleanup 2>&1 |");
  select(STDOUT); $|=1; # Buffer ausschalten
  while (<$fh>)
  {
    # Ausgaben von suck anders aufbereiten
    if (/^---(\d+)\+\+\+/)
    {
      print "$1    \r";
    }
    else
    {
      logprint "SUCK: $_";
      print "SUCK: $_";
    }
  }
  close $fh;

  KomprimiereBatch("batch");

  # Die neue newsrc wieder zurückkopieren
  copy("$root${slash}sucknewsrc", $newsrc);
}


sub KomprimiereBatch
{
  # Es wird im aktuellen Verzeichnis der Batch komprimiert
  # Parameter: Dateiname des Batches, Zielname des Batches
  # Return: -
  #
  my $batch = shift;

  unless (-e $batch)
  {
    warning "Batchfile '$batch' existiert nicht.\n";
    return;
  }

  logprint "Komprimiere '$batch'...\n";
  print "Komprimiere '$batch'...\n";

  my $date = longdate();
  if ($compression eq 'none')
  {
    rename $batch, $batch-$date if $timestamp;
    return;
  }
  if ($compression eq 'zip')
  {
    system("zip -m $batch $batch");
    rename "$batch.zip", "$batch-$date.zip" if $timestamp;
    return;
  }
  if ($compression eq 'gzip')
  {
    system("gzip -9 $batch");
    rename "$batch.gz", "$batch-$date.gz" if $timestamp;
    return;
  }
  if ($compression eq 'bzip2')
  {
    system("bzip2 -9 $batch");
    rename "$batch.bz2", "$batch-$date.bz2" if $timestamp;
    return;
  }

}

sub ErstelleSuckNewsrc
{
  # Erstellt die Newsrc für Suck
  #

  # Die alte sucknewsrc einlesen
  my %oldsucknewsrc = ();
  if (-r $newsrc)
  {
    my $fh = FileHandle->new();
    open($fh, $newsrc) || die "Kann Datei '$newsrc' nicht lesen!\n";
    while (<$fh>)
    {
      # Die sucknewsrc ist so aufgebaut:
      # tum.info.modem 2148
      /(^.+)\s+(-?\d+)/;
      $oldsucknewsrc{$1} = $2;
    }
    close $fh;
    copy($newsrc, "$newsrc.old");
  }

  # Neue (aktuelle) sucknewsrc erstellen
  my $fh = FileHandle->new();
  open($fh, ">$newsrc") || die "Kann Datei '$newsrc' nicht anlegen!\n";
  foreach $gruppe (keys %newsgroups)
  {
    if (defined $oldsucknewsrc{$gruppe})
    {
      # Gruppe war bereits abonniert
      print $fh "$gruppe $oldsucknewsrc{$gruppe}\n";
    }
    else
    {
      # Gruppe wurde neu abonniert
      print $fh "$gruppe -$newgrouparticles\n";
    }
  }
  close $fh;

  # Diese Datei ins Arbeitsverzeichnis kopieren:
  copy($newsrc, "$root${slash}sucknewsrc");
}


sub ReadConfigFile
{
  # Es wird die Konfigurationsdatei eingelesen
  # Parameter: -
  # Return:    -
  #
  my $file = "./${appname}rc";
  $file = "$ENV{HOME}/.${appname}rc" unless -r $file;
  $file = "/etc/${appname}rc" unless -r $file;
  die "Kann weder './${appname}rc' noch '$ENV{HOME}/.${appname}rc' noch ".
  "'/etc/${appname}rc' lesen!\n" unless -r $file;

  # Das Konfigurationsfile muß Mode 600 sein und dem User gehören
  if ( (stat($file))[2] != 33152 ) # "-rw-------"
  {
    logdie "File '$file' muß Mode 600 sein!\n" 
  }
  unless (-o $file)
  {
    logdie "Sie sind nicht Besitzer von '$file'\n"
  }

  my $fh = FileHandle->new();
  open($fh, $file);
  while(<$fh>)
  {
    next if /^\#/; # Kommentare überspringen
    $newsserver       = $1    if /^NEWSSERVER\s+(.+)/io;
    $root             = $1    if /^WORKINGDIR\s+(.+)/io;
    $newsrc           = $1    if /^NEWSRC\s+(.+)/io;
    $logfile          = $1    if /^LOGFILE\s+(.+)/io;
    $logLevel         = $1    if /^LOGLEVEL\s+(.+)/io;
    $compression      = lc $1 if /^COMPRESSION\s+(.+)/io;
    $output           = lc $1 if /^OUTPUT\s+(.+)/io;
    $timestamp        = lc $1 if /^BATCHFILE_WITH_TIMESTAMP\s+(.+)/io;
    $method           = lc $1 if /^METHOD\s+(.+)/io;
    $user             = $1    if /^USER\s+(.+)/io;
    $password         = $1    if /^PASSWORD\s+(.+)/io;
    $newgrouparticles = $1    if /^NEWGROUPARTICLES\s+(\d+)/io;
    last if /^NEWSGROUPS/io;
    next;
  }
  err('NEWSSERVER',               $file) if $newsserver  eq '';
  err('WORKINGDIR',               $file) if $root        eq '';
  err('NEWSRC',                   $file) if $newsrc      eq '';
  err('LOGFILE',                  $file) if $logfile     eq '';
  err('LOGLEVEL',                 $file) if $logLevel    eq '';
  err('COMPRESSION',              $file) if $compression eq '';
  err('OUTPUT',                   $file) if $output      eq '';
  err('BATCHFILE_WITH_TIMESTAMP', $file) if $timestamp   eq '';
  $timestamp = ($timestamp eq 'true') ? $TRUE : $FALSE;
  err('METHOD',                   $file) if $method      eq '';

  unless ($compression =~ /^none$|^zip$|^gzip$|^bzip2$/)
  { die "Eintrag in '$file' für COMPRESSION ist ungültig!\n"; }

  unless ($output =~ /^file$|^stdout$/)
  { die "Eintrag in '$file' für OUTPUT ist ungültig!\n"; }

  unless ($method=~/^perl$|^suck$|^suckthroughperl$/)
  { die "Eintrag in '$file' für METHOD ist ungültig!\n"; }
  if ( ($method =~ /^suck/) && !which('suck') )
  { die "Kann Programm 'suck' nicht auf dem Pfad finden!\n"; }

  while(<$fh>)
  {
    next if /^\#/;   # Kommentare überspringen
    next if /^\s*$/; # Leerzeilen überspringen
    chomp;
    $newsgroups{$_} = $TRUE;
  }

  close $fh;
  $root = KillSlashAtEnd($root);
}

sub err
{
  die "Kein '", shift , "' in '", shift , "' definiert!\n";
}

######################################################################
### Debug, Logging, Exit, ...
######################################################################


sub myexit
{
  # Diese Funktion macht einen normalen exit() mit dem übergebenen
  # Exitcode
  # und erledigt vorher noch Aufräum-Arbeiten, wie LOG-file schließen, ...
  my $error = defined $_[0] ? shift : $NormalExitCode;
  logprint("$appname PID $$ Ende um ",date,"\n");
  close(LOG);
  exit $error;
}


sub logprint
{
  # Schreibt einen Text ins LOG-file
  print LOG @_;
}


sub debug
{
  # Es werden Debug-Informationen erzeugt
  logprint("DEBUG: ",@_) if ($logLevel >= 3);
}


sub warning
{
  # Falls Warnings eingeschaltet sind, dann werden Informationen erzeugt
  logprint("WARN:  ",@_) if ($logLevel >= 2);
}


sub error
{
  # Falls Warnings eingeschaltet sind, dann werden Informationen erzeugt
  logprint("ERROR: ",@_) if ($logLevel >= 1);
}


sub loginit
{
  # Schreibt einen kleinen Header ins LOG-file
  my $login = (getpwuid($<))[0] || 'unknown';
  logprint("\n---\n\n$appname $version PID $$ mit Perl $]\nStart um ",date,
           " durch ",$login,"\n");
}


sub logdie
{
  # Schreibt einen Text ins LOG-file und stirbt dann
  print "FATAL: ",@_;
  logprint("FATAL: ",@_);
  myexit($ErrorExitCode);
}


sub catch_signal 
{
  my $signame = shift;
  logdie "Ende von $appname wegen Signal SIG$signame.\n";
}


sub catch_warning
{
  # Abfangen von Laufzeit-Warnungen
  my $warnung = shift;
  warning "INTERNAL: $warnung     (Interne Warnungen deuten auf einen ",
          "möglichen internen Programm-Fehler\n",
          "     hin oder auf eine fehlerhafte Eingabe, die nicht abgefangen ",
          "wurde!)\n";
  print $warnung if ($logLevel < 2);
}

######################################################################
### Kopf und Hilfe
######################################################################

sub Kopf
{
  my $head = "$appname $version   -   von Stephan Löscher";
  return "\n$head\n" . '~' x length($head) . "\n";
}


sub Hilfe
{
  printumlautepaged
  Kopf().
"Syntax: $appname

$appname holt News per NNTP und erstellt ein rnews-Batchfile.

Alles andere wird in einer Konfigurationsdatei eingestellt, die in dieser
Reihenfolge gesucht wird:
'./${appname}rc', '$ENV{HOME}/.${appname}rc', '/etc/${appname}rc'

Diese Datei hat diesen Aufbau:

NEWSSERVER <Newsserver>
WORKINGDIR <Arbeitsverzeichnis>
NEWSRC <Newsrc für $appname>
LOGFILE <Logfile>
LOGLEVEL <Loglevel>
COMPRESSION <none|zip|gzip|bzip2>
BATCHFILE_WITH_TIMESTAMP <TRUE|FALSE>
OUTPUT <FILE|STDOUT>
METHOD <perl|suck|suckthroughperl>
[ USER <Benutzerkennung auf dem Newsserver> ]
[ PASSWORD <Kennwort auf dem Newsserver> ]
[ NEWGROUPARTICLES <nummer> ]
NEWSGROUPS
<zeilenweise Namen von Newsgruppen>

Die Zeilen in eckigen Klammern [] sind optional.
Mit NEWGROUPARTICLES kann angegeben werden, wieviele Artikel aus einer neu
abonnierten Gruppe geholt werden sollen. Z.B. werden mit '100' die letzten 100
Artikel geholt.

Beispiel:
NEWSSERVER nntpserv
WORKINGDIR /home/loescher/fetchnews
NEWSRC /home/loescher/fetchnews.newsrc
LOGFILE /tmp/fetchnews.log
LOGLEVEL 3
COMPRESSION bzip2
BATCHFILE_WITH_TIMESTAMP TRUE
OUTPUT FILE
METHOD suckthroughperl
USER loescher
PASSWORD sagichnicht
NEWGROUPARTICLES 10000
NEWSGROUPS
de.comp.os.unix.linux.misc
de.comp.os.unix.linux.hardware
de.comp.os.unix.linux.newusers

Die Methoden, die man mit METHOD angeben sind unterscheiden sich wie folgt:
- 'perl' verwendet nur Perl, um die News zu holen
- 'suck' verwendet nur suck, um die News zu holen
- 'suckthroughperl' verwendet suck, um die News zu holen und Perl, um noch
  während des Empfangs ein rnews-Batch zu erstellen.

Mit 'OUTPUT' kann angegeben werden, ob die News in der Datei gespeichert
werden sollen oder noch während des Empfangs auf STDOUT ausgegeben werden
sollen. (Funktioniert nur mit 'perl' und 'suckthroughperl'.)

Die möglichen Loglevels sind: 
0: Nur fatale Fehler
1: zusätzlich alle Fehler (Genaue Fehlerbeschreibungen)
2: zusätzlich alle Warnungen (ganz informativ)
3: zusätzlich alle Debug-Informationen (ausführlicher Status, etc.)

";
exit;
}

######################################################################
#
# Warranty and legal notice
# ~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Copyright (c) 1998 by Stephan Löscher  -  all rights reserved
# My Address: Stephan Löscher, Dr.Troll-str. 3, 82194 Gröbenzell, Germany
# Email: loescher@gmx.de
# WWW: http://www.loescher-online.de/
#
# This program is freeware.
# It is NOT Public-Domain-Software!
# The author (Stephan Löscher) does NOT give up his copyright, but he 
# reserves his copyright. Usage and copying is free of charge for private
# use, but NOT for commercial use!
#
# You may and should copy this program free of charge, use it,
# give it to your friends, upload it to a BBS or something similar, under
# the following conditions:
# * Don't charge any money for it. If you upload it to a BBS, make sure that
#    it can be downloaded free (without paying for downloading it, except
#    for usage fees that have to be paid anyway). Small copying fees (up to
#    5 DM or 3 $US) may be charged.
#  * Only distribute the whole original package, with all the files included.
#  * This program may not be part of any commercial product or service without
#    the written permission by the author.
#  * If you want to include this program on a CD-ROM and/or book, please send
#    me a free copy of the CD/book (this is not a must, but I would appreciate
#    it very much).
#
# Distribution of the program is explicitly desired, provided that the above
# conditions are accepted.
#
# YOU ARE USING THIS PROGRAM AT YOUR OWN RISK! THE AUTHOR (STEPHAN LÖSCHER)
# IS NOT LIABLE FOR ANY DAMAGE OR DATA-LOSS CAUSED BY THE USE OF THIS PROGRAM
# OR BY THE INABILITY TO USE THIS PROGRAM. IF YOU ARE NOT SURE ABOUT THIS, OR
# IF YOU DON'T ACCEPT THIS, THEN DO NOT USE THIS PROGRAM!
# BECAUSE OF THE VARIOUS HARDWARE AND SOFTWARE ENVIRONMENTS INTO WHICH THIS
# PROGRAM MAY BE PUT, NO WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE IS
# OFFERED.
# GOOD DATA PROCESSING PROCEDURE DICTATES THAT ANY PROGRAM BE THOROUGHLY
# TESTED WITH NON-CRITICAL DATA BEFORE RELYING ON IT.
#
# No part of the documentation may be reproduced, transmitted, transcribed,
# stored in any retrieval system, or translated into any other language in
# whole or in part, in any form or by any means, whether it be electronic,
# mechanical, magnetic, optical, manual or otherwise, without prior written
# consent of the author, Stephan Löscher.
#
# You may not make any changes or modifications to this software or this
# manual. You may not decompile, disassemble, or otherwise reverse-engineer
# the software in any way.
# If you got the source, then you are permitted to modify it if you
# contact me and tell me your enhancements.
# You also may include the source as a whole or parts of it into other
# programs, as long as you don't make profit directly out of selling
# the result. If you re-use code of this program then do not remove my name!
# If you include this source-code in your projects, mark it clearly as such
# "... derived from code XXX by Stephan Löscher".
# But don't distribute modified code!
#
# If you believe your copy of this software has been tampered or altered in
# anyway, shape or form, please contact me immediately! Do not hesitate a
# moment to inform me. Remember, this software should be available to all, in
# the original form, so please do not accept modified or damaged versions of
# my software.
#
# The author reserves his right for taking legal steps if the copyright or the
# license agreement is violated.
#
# All product names mentioned in this software are trademarks or registered
# trademarks of their respective owners.
#
# If you have any questions, ideas, suggestions for improvements or if you find
# bugs (I don't hope so.) then feel free to contact me. (Email is appreciated.)
#
# I'm not a native english speaker. If you are one and discover some strange
# sounding parts in this documentation or in the program, please, feel free
# to point it out to me and give me suggestions for alteration!
#
# If the program works for you, and you want to honour my efforts, you are
# invited to donate as much as you want... :)
#
# In any case, if you don't like the restrictions in this license, contact
# me, and we can work something out.
#
######################################################################
