=head1 INN Perl Filtering and Authentication Support

This file documents INN's built-in support for Perl filtering and reader
authentication.  The code is based very heavily on work by Christophe
Wolfhugel <wolf@pasteur.fr>, and his work was in turn inspired by the existing
TCL support.  Please send any bug reports to <inn-workers@lists.isc.org>,
not to Christophe, as the code has been modified heavily since he originally
wrote it.

The Perl filtering support is described in more detail below.  Basically,
it allows you to supply a Perl function that is invoked on every article
received by innd from a peer (the innd filter) or by nnrpd from a reader
(the nnrpd filter).  This function can decide whether to accept or reject
the article, and can optionally do other, more complicated processing
(such as add history entries, cancel articles, spool local posts into a
holding area, or even modify the headers of locally submitted posts).
The Perl authentication hooks allow you to replace or supplement the
readers.conf mechanism used by nnrpd.

For Perl filtering support, you need to have Perl S<version 5.004> or newer.
Earlier versions of Perl will fail with a link error at compilation time.
L<https://www.perl.org/get.html> should have the latest Perl version.

To enable Perl support, you have to specify B<--with-perl> when you run
configure.  See F<INSTALL> for more information.

=head1 The innd Perl Filter

When innd starts, it first loads the file INN_PATH_PERL_STARTUP_INND (defined
in F<include/inn/paths.h>, by default F<startup_innd.pl>) and then loads the
file INN_PATH_PERL_FILTER_INND (also defined in F<include/inn/paths.h>, by
default F<filter_innd.pl>).  Both of these files must be located in the
directory specified by I<pathfilter> in F<inn.conf>.  The default directory for
filter code can be specified at configure time by giving the flag
B<--with-filter-dir> to configure.

INN doesn't care what Perl functions you define in which files.  The only
thing that's different about the two files is when they're loaded.
F<startup_innd.pl> is loaded only once, when innd first starts, and is
never reloaded as long as innd is running.  Any modifications to that file
won't be noticed by innd; only stopping and restarting innd can cause it
to be reloaded.

F<filter_innd.pl>, on the other hand, can be reloaded on command (with
C<ctlinnd reload filter.perl 'reason'>).  Whenever F<filter_innd.pl> is loaded,
including the first time at innd startup, the Perl function
filter_before_reload() is called before it's reloaded and the function
filter_after_reload() is called after it's reloaded (if the functions
exist).  Additionally, any code in either F<startup_innd.pl> or
F<filter_innd.pl> at the top level (in other words, not inside a sub { })
is automatically executed by Perl when the files are loaded.

This allows one to do things like write out filter statistics whenever the
filter is reloaded, load a cache into memory, flush cached data to disk,
or other similar operations that should only happen at particular times or
with manual intervention.  Remember, any code not inside functions in
F<startup_innd.pl> is executed when that file is loaded, and it's loaded
only once when innd first starts.  That makes it the ideal place to put
initialization code that should only run once, or code to load data that
was preserved on disk across a stop and restart of innd (perhaps using
filter_mode() S<-- see> below).

As mentioned above, C<ctlinnd reload filter.perl 'reason'> (or C<ctlinnd reload
all 'reason'>) will cause F<filter_innd.pl> to be reloaded.  If the function
filter_art() is defined after the file has been reloaded, filtering is
turned on.  Otherwise, filtering is turned off.  (Note that due to the way
Perl stores functions, once you've defined filter_art(), you can't
undefine it just by deleting it from the file and reloading the filter.
You'll need to replace it with an empty sub.)

The Perl function filter_art() is the heart of a Perl filter.  Whenever an
article is received from a peer, via either IHAVE or TAKETHIS,
filter_art() is called if Perl filtering is turned on.  It receives no
arguments, and should return a single scalar value.  That value should be
the empty string to indicate that INN should accept the article, or some
rejection message to indicate that the article should be rejected (make sure
that such a message is properly encoded in UTF-8 so as to comply with the
NNTP protocol).

filter_art() has access to a global hash named %hdr, which contains all of
the standard header fields present in the article and their bodies.  The
standard header field names are:

    Also-Control, Approved, Archive, Archived-At, Bytes, Cancel-Key,
    Cancel-Lock, Comments, Content-Base, Content-Disposition,
    Content-Transfer-Encoding, Content-Type, Control, Date,
    Date-Received, Distribution, Expires, Face, Followup-To, From,
    In-Reply-To, Injection-Date, Injection-Info, Jabber-ID, Keywords,
    Lines, List-ID, Message-ID, MIME-Version, Newsgroups,
    NNTP-Posting-Date, NNTP-Posting-Host, NNTP-Posting-Path,
    Organization, Original-Sender, Originator, Path, Posted,
    Posting-Version, Received, References, Relay-Version, Reply-To,
    Sender, Subject, Summary, Supersedes, User-Agent,
    X-Auth, X-Auth-Sender, X-Canceled-By, X-Cancelled-By,
    X-Complaints-To, X-Face, X-HTTP-UserAgent, X-HTTP-Via, X-Mailer,
    X-Modbot, X-Modtrace, X-Newsposter, X-Newsreader, X-No-Archive,
    X-Original-Message-ID, X-Original-NNTP-Posting-Host,
    X-Original-Trace, X-Originating-IP, X-PGP-Key, X-PGP-Sig,
    X-Poster-Trace, X-Postfilter, X-Proxy-User, X-Submissions-To,
    X-Trace, X-Usenet-Provider, X-User-ID, Xref.

Note that all the above header fields are as they arrived, not modified by
your INN (especially, the Xref header field, if present, is the one of
the remote site which sent you the article, and not yours).

For example, the Newsgroups header field body of the article is accessible
inside the Perl filter as C<$hdr{'Newsgroups'}>.  In addition,
C<$hdr{'__BODY__'}> will contain the full body of the article and
C<$hdr{'__LINES__'}> will contain the number of lines in the body of the
article.

The contents of the %hdr hash for a typical article may therefore look
something like this:

    %hdr = (
        Subject      => 'MAKE MONEY FAST!!',
        From         => 'Joe Spamer <him@example.com>',
        Date         => '10 Sep 1996 15:32:28 UTC',
        Newsgroups   => 'alt.test',
        Path         => 'news.example.com!not-for-mail',
        Organization => 'Spammers Anonymous',
        Lines        => '5',
        Distribution => 'usa',
        'Message-ID' => '<6.20232.842369548@example.com>',
        __BODY__     => 'Send five dollars to ISC, c/o ...',
        __LINES__    => 5,
    );

Note that the value of C<$hdr{Lines}> is the body of the (obsolescent and
optional) Lines header field of the article and may bear no resemblance to the
actual length of the article.  C<$hdr{__LINES__}> is the line count calculated
by INN, and is guaranteed to be accurate.

The %hdr hash should not be modified inside filter_art().  Instead, if any
of the contents need to be modified temporarily during filtering (smashing
case, for example), copy them into a separate variable first and perform
the modifications on the copy.  Currently, C<$hdr{__BODY__}> is the only
data that will cause your filter to die if you modify it, but in the
future other keys may also contain live data.  Modifying live INN data in
Perl will hopefully only cause a fatal exception in your Perl code that
disables Perl filtering until you fix it, but it's possible for it to
cause article munging or even core dumps in INN.  So always, always make a
copy first.

As mentioned above, if filter_art() returns the empty string (''), the
article is accepted.  Note that this must be the empty string, not 0 or
undef.  Otherwise, the article is rejected, and whatever scalar
filter_art() returns (typically a string) will be taken as the reason why
the article was rejected (make sure that such a message is properly encoded
in UTF-8 so as to comply with the NNTP protocol).  This reason will be
returned to the remote peer as well as logged to the news logs.  (innreport,
in its nightly report, will summarize the number of articles rejected
by the Perl filter and include a count of how many articles were rejected
with each reason string.)

One other type of filtering is also supported.  If Perl filtering is turned
on and the Perl function filter_messageid() is defined, that function will
be called for each message-ID received from a peer (via either CHECK, IHAVE
or TAKETHIS).  The function receives a single argument, the message-ID,
and like filter_art() should return an empty string to accept the article
or an error string to refuse the article (make sure that such a message
is properly encoded in UTF-8 so as to comply with the NNTP protocol).
This function is called before any history lookups and for every article
offered to B<innd> with CHECK or IHAVE (before the actual article is sent),
or with TAKETHIS (after the actual article is sent).  Accordingly, the
message-ID is the only information it has about the article (the %hdr hash
will be empty).  This code would sit in a performance-critical hot path in
a typical server, and therefore should be as fast as possible, but it can
do things like refuse articles from certain hosts or cancels for already
rejected articles (if they follow the $alz convention) without having to
take the network bandwidth hit of accepting the entire article first.

Finally, whenever ctlinnd throttle, ctlinnd pause, or ctlinnd go is run,
the Perl function filter_mode() is called if it exists.  It receives no
arguments and returns no value, but it has access to a global hash %mode
that contains three values:

    Mode       The current server mode (throttled, paused, or running)
    NewMode    The new mode the server is going to
    reason     The reason that was given to ctlinnd

Mode and NewMode will be set to one of running, paused, throttled, or
shutdown, except that Mode can never be shutdown.  If NewMode is shutdown,
innd is being shut down immediately after the filter hooks return.  One
possible use for this function is to save filter state across a restart of
innd by dumping state to disk when NewMode is shutdown and then reloading
it when innd restarts (possibly by F<startup_innd.pl>).

The state of the Perl interpreter in which all of these Perl functions run
is preserved over the lifetime of innd.  In other words, it's permissible for
the Perl code to create its own global Perl variables, data structures,
saved state, and the like, and all of that will be available to
filter_art() and filter_messageid() each time they're called.  The only
variable INN fiddles with (or pays any attention to at all) is %hdr, which
is cleared after each call to filter_art().

Perl filtering can be turned off with C<ctlinnd perl n> and back on again
with C<ctlinnd perl y>.  Perl filtering is turned off automatically if
loading of the filter fails or if the filter code returns any sort of a
fatal error (either due to Perl itself or due to a C<die> in the Perl code).
When filtering is disabled and a C<filter_end()> function is available,
it will be called prior to the deactivation of the filter.

=head1 Supported innd Callbacks

innd makes seven functions available to any of its embedded Perl code.
Those are:

=over 4

=item INN::addhist(I<messageid>, I<arrival>, I<articledate>, I<expire>,
I<paths>)

Adds I<messageid> to the history database.  All of the arguments except
the first one are optional; the times default to the current time and the
paths field defaults to the empty string.  (For those unfamiliar with
the fields of a history(5) database entry, the I<arrival> is normally
the time at which the server accepts the article, the I<articledate>
is from the Date header field of the article, the I<expire> is from
the Expires header field of the article, and the I<paths> field is
the storage API token.  All three times as measured as a time_t since
the epoch.)  Returns true on success, false otherwise.

=item INN::article(I<messageid>)

Returns the full article (as a simple string) identified by I<messageid>,
or undef if it isn't found.  Each line will end with a simple \n, but
leading periods may still be doubled if the article is stored in wire
format.

=item INN::cancel(I<messageid>)

Cancels I<messageid>.  (This is equivalent to C<ctlinnd cancel>; it
cancels the message on the local server, but doesn't post a cancel message
or do anything else that affects anything other than the local server.)
Returns true on success, false otherwise.

=item INN::filesfor(I<messageid>)

Returns the I<paths> field of the history entry for the given
I<messageid>.  This will be the storage API token for the message.  If
I<messageid> isn't found in the history database, returns undef.

=item INN::havehist(I<messageid>)

Looks up I<messageid> in the history database and returns true if it's
found, false otherwise.

=item INN::head(I<messageid>)

Returns the header (as a simple string) of the article identified by
I<messageid>, or undef if it isn't found.  Each line will end with a
simple \n (in other words, regardless of the format of article storage,
the returned string won't be in wire format).

=item INN::newsgroup(I<newsgroup>)

Returns the status of I<newsgroup> (the last field of the F<active> file
entry for that newsgroup).  See active(5) for a description of the
possible values and their meanings (the most common are C<y> for an
unmoderated group and C<m> for a moderated group).  If I<newsgroup> isn't
in the F<active> file, returns C<undef>.

=back

These functions can only be used from inside the innd Perl filter; they're
not available in the nnrpd filter.

=head1 Common Callbacks

The following additional function is available from inside filters
embedded in innd, and is also available from filters embedded in nnrpd
(see below):

=over 4

=item INN::syslog(level, message)

Logs a message via syslog(3).  This is quite a bit more reliable and
portable than trying to use C<Sys::Syslog> from inside the Perl filter.  Only
the first character of the level argument matters; the valid letters are
the first letters of ALERT, CRIT, ERR, WARNING, NOTICE, INFO, and DEBUG
(case-insensitive) and specify the priority at which the message is
logged.  If a level that doesn't match any of those levels is given, the
default priority level is LOG_NOTICE.  The second argument is the message
to log; it will be prefixed by "filter: " and logged to syslog with
facility LOG_NEWS.

=back

=head1 The nnrpd Posting Filter

Whenever Perl support is needed in nnrpd, it first loads the file
INN_PATH_PERL_FILTER_NNRPD (defined in F<include/inn/paths.h>, by default
F<filter_nnrpd.pl>).  This file must be located in the directory
specified by I<pathfilter> in F<inn.conf>.  The default directory for
filter code can be specified at configure time by giving the flag
B<--with-filter-dir> to configure.

If F<filter_nnrpd.pl> loads successfully and defines the Perl function
filter_post(), Perl filtering is turned on.  Otherwise, it's turned off.
If filter_post() ever returns a fatal error (either from Perl or from a
C<die> in the Perl code), Perl filtering is turned off for the life of
that nnrpd process and any further posts made during that session won't go
through the filter.  Besides, if F<filter_nnrpd.pl> defines a C<filter_end()>
function, it will be called prior to the deactivation of the filter.

Perl filtering can also be disabled for specific users matching access groups
with the use of the I<perlfilter> parameter in F<readers.conf>.

While Perl filtering is on, every article received by nnrpd via the POST
command is passed to the filter_post() Perl function before it is passed
to INN (or mailed to the moderator of a moderated newsgroup).  If
filter_post() returns an empty string (''), the article is accepted and
normal processing of it continues.  Otherwise, the article is rejected and
the string returned by filter_post() is returned to the client as the
error message (with some exceptions; see below).

filter_post() has access to a global hash %attributes which contains
information about the connection as follows: C<$attributes{'hostname'}>
will contain the hostname (or the IP address if it does not resolve)
of the client machine, C<$attributes{'ipaddress'}> will contain its IP
address (as a string), C<$attributes{'port'}> will contain the client
port (as an integer), C<$attributes{'interface'}> contains the hostname
of the interface the client connected on, C<$attributes{'intipaddr'}>
contains the IP address (as a string) of the interface the client
connected on, and C<$attributes{'intport'}> contains the port (as an
integer) on the interface the client connected on.

filter_post() also has access to a global hash C<%hdr>, which contains all
of the header fields of the article.  (Unlike the B<innd> Perl filter, C<%hdr>
for the B<nnrpd> Perl filter contains *all* of the header fields, not just the
standard ones.  If any of the header fields are duplicated, though, C<%hdr>
will contain only the body of the last occurrence of the header field, whereas
it is the first occurrence with the B<innd> Perl filter.  B<nnrpd> will
reject the article before the filter runs if any of the standard header fields
are duplicated.)  It also has access to the full body of the article in
the variable C<$body>, and if the poster authenticated via AUTHINFO (or
if either Perl authentication or a F<readers.conf> authentication method
is used and produces user information), it has access to the authenticated
username of the poster in the variable C<$user>.

Unlike the innd Perl filter, the nnrpd Perl filter can modify the %hdr
hash.  In fact, if the Perl variable $modify_headers is set to true after
filter_post() returns, the contents of the %hdr hash will be written back
to the article replacing the original header fields.  filter_post() can
therefore make any modifications it wishes to the headers and those
modifications will be reflected in the article as it's finally posted.
The article body cannot be modified in this way; any changes to $body will
just be ignored.

Be careful when using the ability to modify header fields.  filter_post()
runs after all the normal consistency checks on the header fields
and after server supplied header fields (like Message-ID and Date)
are filled in.  Deleting required header fields or modifying header
fields that need to follow a strict format can result in nnrpd trying to
post nonsense articles (which will probably then be rejected by innd).
If $modify_headers is set, I<everything> in the %hdr hash is taken to
be article header fields and added to the article.  If a header field
body contains several lines, they are separated from each other by a
mere LF (B<nnrpd> will properly rewrite them as CRLF when posting).

To modify the contents of the Organization header field body, you can use:

    $hdr{'Organization'} = 'My new organization';
    $modify_headers = 1;

To delete the Organization header field, you should not use
C<delete $hdr{'Organization'}> (this syntax may work with
some header fields, and not with other header fields).  Instead, you are
encouraged to use:

    $hdr{'Organization'} = undef;
    $modify_headers = 1;

or:

    $hdr{'Organization'} = '';
    $modify_headers = 1;

If filter_post() returns something other than the empty string, this
message is normally returned to the client as an error (make sure that
such a message is properly encoded in UTF-8 so as to comply with the
NNTP protocol).  There are three exceptions:

=over 4

=item *

If the string returned begins with C<CLOSE>, the post will be discarded
and the connection closed with a C<400> response code.

=item *

If the string returned begins with C<DROP>, the post will be
silently discarded and success returned to the client.

=item *

If the string begins with C<SPOOL>, success is returned to the client, but
the post is saved in a directory named F<spam> under the directory specified by
I<pathincoming> in F<inn.conf> (in a directory named F<spam/mod> if the post
is to a moderated group).  This is intended to allow manual inspection of
the suspect messages; if they should be posted, they can be manually moved
out of the subdirectory to the directory specified by I<pathincoming> in
F<inn.conf>, where they can be posted by running C<rnews -U>.  If you use
this functionality, make sure those directories exist.

=back

=head1 Perl Authentication Support for nnrpd

This section deals with the authentication mechanism introduced in
S<INN 2.4.0> by Erik Klavon, combining the old one with the F<readers.conf>
facilities.

Support for authentication via Perl is provided in B<nnrpd> by the
inclusion of a I<perl_auth> parameter in a F<readers.conf> auth
group.  I<perl_auth> works exactly like the I<auth> parameter in
F<readers.conf>, except that it calls the script given as argument using
the Perl hook rather than treating it as an external program:

    perl_auth: "/path/to/script/auth_script.pl"

If the processing of readers.conf requires that a perl_auth: statement
be used for authentication, Perl is loaded (if it has yet to be) and
the file given as argument to the perl_auth: parameter is loaded as
well.  If a Perl function auth_init() is defined by that file, it is called
immediately after the file is loaded.  It takes no arguments and returns
nothing.

Provided the file loads without errors, auth_init() (if present) runs
without fatal errors, and a Perl function authenticate() is defined,
authenticate() will then be called.  authenticate() takes no arguments,
but it has access to a global hash %attributes which contains
information about the connection as follows: C<$attributes{hostname}>
will contain the hostname (or the IP address if it doesn't resolve) of
the client machine, C<$attributes{ipaddress}> will contain its IP
address (as a string), C<$attributes{port}> will contain the client
port (as an integer), C<$attributes{interface}> contains the hostname
of the interface the client connected on, C<$attributes{intipaddr}>
contains the IP address (as a string) of the interface the client
connected on, C<$attributes{intport}> contains the port (as an
integer) on the interface the client connected on,
C<$attributes{username}> will contain the provided username and
C<$attributes{password}> the password.

authenticate() should return a two or three element array.  The first
element is the NNTP response code to return to the client, the second
element is an error string which is passed to the client if the response
code indicates that the authentication attempt has failed (make sure
that such a message is properly encoded in UTF-8 so as to comply with the
NNTP protocol).  This allows a specific error message to be generated by
the Perl script in place of C<Authentication failed>.  An optional third
return element if present will be used to match the connection with the
users: parameter in access groups and will also be the username logged.
If this element is absent, the username supplied by the client during
authentication will be used for matching and logging.

The NNTP response code should be 281 (authentication successful),
481 (authentication unsuccessful), or 403 (server failure).  If the
code returned is anything other than these three values, B<nnrpd>
will use 403.

If authenticate() dies (either due to a Perl error or due to calling die),
or if it returns anything other than the two or three element array
described above, an internal error will be reported to the client, the
exact error will be logged to syslog, and B<nnrpd> will drop the
connection and exit with a 400 response code.

=head1 Dynamic Generation of Access Groups

A Perl script may be used to dynamically generate an access group
which is then used to determine the access rights of the client.
This occurs whenever the perl_access: is specified in an auth group
which has successfully matched the client.  This concept is due to
Jeffrey S<M. Vinocur>.

Only one perl_access: statement is allowed in an auth group.  This
parameter should not be mixed with a python_access: statement in the
same auth group.

When a perl_access: parameter is encountered, Perl is loaded (if it
has yet to be) and the file given as argument is loaded as
well.  Provided the file loads without errors, and a Perl function
access() is defined, access() will then be called.  access() takes no
arguments, but it has access to a global hash %attributes which
contains information about the connection as follows:
C<$attributes{hostname}> will contain the hostname (or the IP address
if it doesn't resolve) of the client machine,
C<$attributes{ipaddress}> will contain its IP address (as a string),
C<$attributes{port}> will contain the client port (as an integer),
C<$attributes{interface}> contains the hostname of the interface the
client connected on, C<$attributes{intipaddr}> contains the IP address
(as a string) of the interface the client connected on,
C<$attributes{intport}> contains the port (as an integer) on the
interface the client connected on, C<$attributes{username}> will
contain the provided username and domain (in username@domain form).

access() returns a hash, containing the desired access parameters and
values.  Here is an untested example showing how to dynamically generate a
list of newsgroups based on the client's username and domain.

    my %hosts = (
        "example.com" => "example.*",
        "isc.org" => "isc.*",
    );

    sub access {
        %return_hash = (
            "max_rate" => "10000",
            "addinjectionpostinghost" => "true",
            # ...
        );
        if (defined $attributes{username}
            && $attributes{username} =~ /.*@(.*)/)
        {
            $return_hash{"virtualhost"} = "true";
            $return_hash{"path"} = $1;
            $return_hash{"newsgroups"} = $hosts{$1};
        } else {
            $return_hash{"read"} = "*";
            $return_hash{"post"} = "local.*";
        }
        return %return_hash;
    }

Note that both the keys and values are quoted strings.  These values
are to be returned to a C program and must be quoted strings.  For
values containing one or more spaces, it is not necessary to include
extra quotes inside the string.

While you may include the users: parameter in a dynamically generated
access group, some care should be taken (unless your pattern is just
* which is equivalent to leaving the parameter out).  The group created
with the values returned from the Perl script is the only one
considered when nnrpd attempts to find an access group matching the
connection.  If a users: parameter is included and it doesn't match the
connection, then the client will be denied access since there are no
other access groups which could match the connection.

If access() dies (either due to a Perl error or due to calling die),
or if it returns anything other than a hash as described
above, an internal error will be reported to the client, the exact error
will be logged to syslog, and nnrpd will drop the connection and exit.

=head1 Notes on Writing Embedded Perl

All Perl evaluation is done inside an implicit eval block, so calling die
in Perl code will not kill the innd or nnrpd process.  Neither will Perl
errors (such as syntax errors).  However, such errors will have negative
effects (fatal errors in the innd or nnrpd filter will cause filtering to
be disabled, and fatal errors in the nnrpd authentication code will cause
the client connection to be terminated).

Calling exit directly, however, *will* kill the innd or nnrpd process, so
don't do that.  Similarly, you probably don't want to call fork (or any
other function that results in a fork such as system, C<IPC::Open3::open3()>,
or any use of backticks) since there are possibly unflushed buffers that
could get flushed twice, lots of open state that may not get closed
properly, and innumerable other potential problems.  In general, be aware
that all Perl code is running inside a large and complicated C program,
and Perl code that impacts the process as a whole is best avoided.

You can use print and warn inside Perl code to send output to STDOUT or
STDERR, but you probably shouldn't.  Instead, open a log file and print to
it instead (or, in the innd filter, use C<INN::syslog()> to write messages
via syslog like the rest of INN).  If you write to STDOUT or STDERR, where
that data will go depends on where the filter is running; inside innd, it
will go to the news log or the errlog, and inside nnrpd it will probably
go nowhere but could go to the client.  The nnrpd filter takes some steps
to try to keep output from going across the network connection to the
client (which would probably result in a very confused client), but best
not to take the chance.

For similar reasons, try to make your Perl code -w clean, since Perl
warnings are written to STDERR.  (INN won't run your code under -w, but
better safe than sorry, and some versions of Perl have some mandatory
warnings you can't turn off.)

You *can* use modules in your Perl code, just like you would in an
ordinary Perl script.  You can even use modules that dynamically load C
code.  Just make sure that none of the modules you use go off behind your
back to do any of the things above that are best avoided.

Whenever you make any modifications to the Perl code, and particularly
before starting INN or reloading filter.perl with new code, you should run
perl -wc on the file.  This will at least make sure you don't have any
glaring syntax errors.  Remember, if there are errors in your code,
filtering will be disabled, which could mean that posts you really wanted
to reject will leak through and authentication of readers may be totally
broken.

The samples directory has example F<startup_innd.pl>, F<filter_innd.pl>,
F<filter_nnrpd.pl>, and F<nnrpd_auth.pl> files that contain some
simplistic examples.  Look them over as a starting point when writing your
own.

=head1 Available Packages

This is an unofficial list of known filtering packages at the time of
publication.  This is not an endorsement of these filters by ISC or
the INN developers, but is included as assistance in locating packages
which make use of this filter mechanism.

=over 4

=item cleanfeed-ng

URL: L<https://github.com/infybofh/cleanfeed-ng/> (maintained by Ivo Gandolfo)

cleanfeed-ng is a maintained, lightweight continuation of the historical
Cleanfeed filter.  It is designed for server-to-server Usenet transit
filtering, with particular attention to predictable behaviour, low overhead,
safe configuration changes, and useful diagnostics.

cleanfeed-ng is an extremely powerful spam filter, probably the most widely
used spam filter on Usenet currently.  It catches excessive multiposting and
a host of other things, and is highly configurable.  It uses F<filter_innd.pl>
exclusively and requires the MD5 Perl module.

Since 2026, cleanfeed-ng has continued the historical work of Cleanfeed,
originally developed by Jeremy Nixon, who maintained it until 1998.
Further development was then taken on by Marco d'Itri until 2002, followed
by later updates and maintenance by Steve Crook from 2007 to 2020 (at
L<http://www.mixmin.net/cleanfeed/> but no longer maintained).

=item Postfilter-NG

URL: L<https://github.com/infybofh/postfilter-ng/> (maintained by Ivo Gandolfo)

Postfilter-NG is an B<nnrpd> Perl filter (F<filter_nnrpd.pl>) which scans
all messages sent by local users in order to block abuses.  Postfilter-NG
validates articles before acceptance, applies text and binary policies,
records searchable audit events in SQLite, and supports staged deployment
through audit mode.  It can limit the number of messages per user, IP or
domain, provides a set of powerful ban lists, supports RBLs and URIBLs,
can handle connections that come from TOR and is able to deeply modify the
headers.  It is designed for large sites.

Since 2026, Postfilter-NG has continued the historical work of Postfilter,
originally developed in 2005 by Paolo Amoroso, who maintained it until 2020.

=item News Shogun

URL: L<http://www.panix.com/shogun/> (by Aidan Cully)

A posting filter for helping a site enforce Usenet-II soundness,
and for quota'ing the number of messages any user can post to
Usenet daily.  It uses F<filter_nnrpd.pl>.

The project is no longer maintained.

=back

=cut
