Author: vetinari
Date: Wed Aug  8 10:25:54 2007
New Revision: 769

Added:
   trunk/docs/
   trunk/docs/plugins.pod

Log:
Plugin documentation

Added: trunk/docs/plugins.pod
==============================================================================
--- (empty file)
+++ trunk/docs/plugins.pod      Wed Aug  8 10:25:54 2007
@@ -0,0 +1,1421 @@
+#
+# This file is best read with ``perldoc plugins.pod''
+#
+
+### 
+# Conventions:
+#  plugin names:  F<myplugin>, F<qpsmtpd-async>
+#  constants:     I<LOGDEBUG>
+#  smtp commands, answers: B<HELO>, B<250 Queued!>
+#
+# Notes: 
+#  * due to restrictions of some POD parsers, no C<<$object->method()>>
+#    are allowed, use C<$object-E<gt>method()> 
+# 
+
+=head1 Introduction
+
+Plugins are the heart of qpsmtpd. The core implements only basic SMTP protocol
+functionality. No useful function can be done by qpsmtpd without loading 
+plugins.
+
+Plugins are loaded on startup where each of them register their interest in 
+various I<hooks> provided by the qpsmtpd core engine.
+
+At least one plugin B<must> allow or deny the B<RCPT> command to enable 
+receiving mail. The F<check_relay> plugin is the standard plugin for this.
+Other plugins provide extra functionality related to this; for example the
+F<require_resolvable_fromhost> plugin.
+
+=head2 Loading Plugins
+
+The list of plugins to load are configured in the I<config/plugins>
+configuration file. One plugin per line, empty lines and lines starting
+with I<#> are ignored. The order they are loaded is the same as given 
+in this config file. This is also the order the registered I<hooks>
+are run. The plugins are loaded from the F<plugins/> directory or 
+from a subdirectory of it. If a plugin should be loaded from such a 
+subdirectory, the directory must also be given, like the 
+F<virus/clamdscan> in the example below. Alternate plugin directories
+may be given in the F<config/plugin_dirs> config file, one directory
+per line, these will be searched first before using the builtin fallback
+of F<plugins/> relative to the qpsmtpd root directory. It may be 
+necessary, that the F<config/plugin_dirs> must be used (if you're using 
+F<Apache::Qpsmtpd>, for example).
+
+Some plugins may be configured by passing arguments in the F<plugins>
+config file.
+
+A plugin can be loaded two or more times with different arguments by adding
+I<:N> to the plugin filename, with I<N> being a number, usually starting at 
+I<0>. 
+
+Another method to load a plugin is to create a valid perl module, drop this
+module in perl's C<@INC> path and give the name of this module as
+plugin name. The only restriction to this is, that the module name B<must>
+contain I<::>, e.g. C<My::Plugin> would be ok, C<MyPlugin> not. Appending of
+I<:0>, I<:1>, ... does not work with module plugins.
+
+ check_relay
+ virus/clamdscan
+ spamassassin reject_threshold 7
+ my_rcpt_check example.com
+ my_rcpt_check:0 example.org
+ My::Plugin
+
+=head1 Anatomy of a plugin
+
+A plugin has at least one method, which inherits from the 
+C<Qpsmtpd::Plugin> object. The first argument for this method is always the 
+plugin object itself (and usually called C<$self>). The most simple plugin 
+has one method with a predefined name which just returns one constant.
+
+ # plugin temp_disable_connection
+ sub hook_connect {
+    return(DENYSOFT, "Sorry, server is temporarily unavailable.");
+ }
+
+While this is a valid plugin, it is not very useful except for rare 
+circumstances. So let us see what happens when a plugin is loaded.
+
+=head2 Initialisation
+
+After the plugin is loaded the C<init()> method of the plugin is called, 
+if present. The arguments passed to C<init()> are 
+
+=over 4
+
+=item $self 
+
+the current plugin object, usually called C<$self>
+
+=item $qp
+
+the Qpsmtpd object, usually called C<$qp>.
+
+=item @args
+
+the values following the plugin name in the F<plugins> config, split by 
+white space. These arguments can be used to configure the plugin with 
+default and/or static config settings, like database paths, 
+timeouts, ...
+
+=back
+
+This is mainly used for inheriting from other plugins, but may be used to do
+the same as in C<register()>.
+
+The next step is to register the hooks the plugin provides. Any method which 
+is named C<hook_$hookname> is automagically added.
+
+Plugins should be written using standard named hook subroutines. This 
+allows them to be overloaded and extended easily. Because some of the 
+callback names have characters invalid in subroutine names , they must be 
+translated. The current translation routine is C<s/\W/_/g;>. If you choose 
+not to use the default naming convention, you need to register the hooks in 
+your plugin in the C<register()> method (see below) with the 
+C<register_hook()> call on the plugin object.
+
+  sub register {
+    my ($self, $qp, @args) = @_;
+    $self->register_hook("mail", "mail_handler");
+    $self->register_hook("rcpt", "rcpt_handler");
+  }
+  sub mail_handler { ... }
+  sub rcpt_handler { ... }
+
+The C<register()> method is called last. It receives the same arguments as 
+C<init()>. There is no restriction, what you can do in C<register()>, but 
+creating database connections and reuse them later in the process may not be
+a good idea. This initialisation happens before any C<fork()> is done. 
+Therefore the file handle will be shared by all qpsmtpd processes and the 
+database will probably be confused if several different queries arrive on 
+the same file handle at the same time (and you may get the wrong answer, if
+any). This is also true for F<qpsmtpd-async> and the pperl flavours, but 
+not for F<qpsmtpd> started by (x)inetd or tcpserver. 
+
+In short: don't do it if you want to write portable plugins.
+
+=head2 Inheritance
+
+Inheriting methods from other plugins is an advanced topic. You can alter 
+arguments for the underlying plugin, prepare something for the I<real> 
+plugin or skip a hook with this. Instead of modifying C<@ISA>
+directly in your plugin, use the C<isa_plugin()> method from the 
+C<init()> subroutine.
+
+  # rcpt_ok_child
+  sub init {
+    my ($self, $qp, @args) = @_;
+    $self->isa_plugin("rcpt_ok");
+  }
+  
+  sub hook_rcpt {
+    my ($self, $transaction, $recipient) = @_;
+    # do something special here...
+    $self->SUPER::hook_rcpt($transaction, $recipient);
+  }
+
+=head2 Config files
+
+Most of the existing plugins fetch their configuration data from files in the
+F<config/> sub directory. This data is read at runtime and may be changed
+without restarting qpsmtpd. 
+B<(FIXME: caching?!)>
+The contents of the files can be fetched via
+
+  @lines = $self->qp->config("my_config");
+
+All empty lines and lines starting with C<#> are ignored.
+
+If you don't want to read your data from files, but from a database you can 
+still use this syntax and write another plugin hooking the C<config>
+hook. 
+
+=head2 Logging
+
+Log messages can be written to the log file (or STDERR if you use the 
+F<logging/warn> plugin) with 
+
+  $self->qp->log($loglevel, $logmessage);
+
+The log level is one of (from low to high priority)
+
+=over 4
+
+=item LOGDEBUG
+
+=item LOGINFO
+
+=item LOGNOTICE
+
+=item LOGWARN
+
+=item LOGERROR
+
+=item LOGCRIT
+
+=item LOGALERT
+
+=item LOGEMERG
+
+=back
+
+While debugging your plugins, you want to set the log level in the F<logging>
+config file to I<LOGDEBUG>. This will log very much data. To restrict this 
+output just to the plugin you are debugging, you can use the following plugin: 
+
+=cut
+
+FIXME: Test if this really works as inteded ;-)
+
+=pod
+
+ # logging/debug_plugin - just show LOGDEBUG messages of one plugin
+ # Usage: 
+ #  logging/debug_plugin my_plugin LOGLEVEL
+ #
+ #  LOGLEVEL is the log level for all other log messages
+ use Qpsmtpd::Constants;
+ 
+ sub register {
+   my ($self, $qp, $plugin, $loglevel) = @_;
+   die "no plugin name given"
+     unless $plugin;
+   $loglevel = "LOGWARN"
+     unless defined $loglevel;
+   $self->{_plugin} = $plugin;
+   $self->{_level}  = Qpsmtpd::Constants::log_level($loglevel);
+   $self->{_level}  = LOGWARN 
+     unless defined $self->{_level};
+ }
+ 
+ sub hook_logging {
+   my ($self, $transaction, $trace, $hook, $plugin, @log) = @_;
+   return(OK) # drop these lines
+     if $plugin ne $self->{_plugin} and $trace > $self->{_level};
+   return(DECLINED);
+ }
+
+The above plugin should be loaded before the default logging plugin, which 
+logs with I<LOGDEBUG>. The plugin name must be the one returned by the 
+C<plugin_name()> method of the debugged plugin. This is probably not 
+the same as the name of the plugin (i.e. not the same you write in the 
+F<plugins> config file). In doubt: take a look in the log file for lines 
+like C<queue::qmail_2dqueue hooking queue> (here: F<queue/qmail-queue> 
+=E<gt> F<queue::qmail_2dqueue>).
+
+=head2 Information about the current plugin
+
+Each plugin inherits the public methods from C<Qpsmtpd::Plugin>. 
+
+=over 4
+
+=item plugin_name()
+
+Returns the name of the currently running plugin
+
+=item hook_name()
+
+Returns the name of the running hook
+
+=item auth_user()
+
+Returns the name of the user the client is authed as (if authentication is 
+used, of course)
+
+=item auth_mechanism()
+
+Returns the auth mechanism if authentication is used
+
+=item connection()
+
+Returns the C<Qpsmtpd::Connection> object associated with the current 
+connection
+
+=item transaction()
+
+Returns the C<Qpsmtpd::Transaction> object associated with the current 
+transaction
+
+=back
+
+=head2 Temporary Files
+
+The temporary file and directory functions can be used for plugin specific 
+workfiles and will automatically be deleted at the end of the current 
+transaction.
+
+=over 4
+
+=item temp_file()
+
+Returns a unique name of a file located in the default spool directory, 
+but does not open that file (i.e. it is the name not a file handle).
+
+=item temp_dir()
+
+Returns the name of a unique directory located in the default spool 
+directory, after creating the directory with 0700 rights. If you need a 
+directory with different rights (say for an antivirus daemon), you will
+need to use the base function C<$self-E<gt>qp-E<gt>temp_dir()>, which takes a 
+single parameter for the permissions requested (see L<mkdir> for details). 
+A directory created like this will not be deleted when the transaction 
+is ended.
+
+=item spool_dir()
+
+Returns the configured system-wide spool directory.
+
+=back
+
+
+=head2 Connection and Transaction Notes
+
+Both may be used to share notes across plugins and/or hooks. The only real
+difference is their life time. The connection notes start when a new 
+connection is made and end, when the connection ends. This can, for example,
+be used to count the number of none SMTP commands. The plugin which uses 
+this is the F<count_unrecognized_commands> plugin from the qpsmtpd core
+distribution.
+
+The transaction note starts after the B<MAIL FROM: > command and are just 
+valid for the current transaction, see below in the I<reset_transaction>
+hook when the transaction ends.
+
+
+=head1 Return codes
+
+Each plugin must return an allowed constant for the hook and (usually)
+optionally a ``message'' for the client.
+Generally all plugins for a hook are processed until one returns 
+something other than I<DECLINED>.
+
+Plugins are run in the order they are listed in the F<plugins> 
+configuration file.
+
+The return constants are defined in C<Qpsmtpd::Constants> and have
+the following meanings:
+
+=over 4
+
+=item DECLINED
+
+Plugin declined work; proceed as usual. This return code is I<always allowed>
+unless noted otherwise.
+
+=item OK
+
+Action allowed.
+
+=item DENY
+
+Action denied.
+
+=item DENYSOFT
+
+Action denied; return a temporary rejection code (say B<450> instead 
+of B<550>).
+
+=item DENY_DISCONNECT
+
+Action denied; return a permanent rejection code and disconnect the client. 
+Use this for "rude" clients. Note that you're not supposed to do this 
+according to the SMTP specs, but bad clients don't listen sometimes.
+
+=item DENYSOFT_DISCONNECT
+
+Action denied; return a temporary rejection code and disconnect the client. 
+See note above about SMTP specs.
+
+=item DONE
+
+Finishing processing of the request. Usually used when the plugin sent the 
+response to the client.
+
+=back
+
+The I<YIELD> constant is not mentioned here, because it is not used by
+plugins directly.
+
+=head1 SMTP hooks
+
+This section covers the hooks, which are run in a normal SMTP connection. 
+The order of these hooks is like you will (probably) see them, while a mail
+is received. 
+
+Every hook receives a C<Qpsmtpd::Plugin> object of the currently 
+running plugin as the first argument. A C<Qpsmtpd::Transaction> object is 
+the second argument of the current transaction in the most hooks, exceptions 
+are noted in the description of the hook. If you need examples how the 
+hook can be used, see the source of the plugins, which are given as 
+example plugins.
+
+=head2 hook_pre_connection
+
+Called by a controlling process (e.g. forkserver or prefork) after accepting 
+the remote server, but before beginning a new instance (or handing the 
+connection to the worker process).
+
+Useful for load-management and rereading large config files at some
+frequency less than once per session.
+
+This hook only works in the F<qpsmtpd-forkserver> and F<qpsmtpd-prefork>
+flavours. 
+
+=cut 
+
+NOT FOR: -async, apache, -server and inetd/pperl
+
+=pod
+
+B<NOTE:> You should not use this hook to do major work and / or use lookup 
+methods which (I<may>) take some time, like DNS lookups. This will slow down
+B<all> incoming connections, no other connection will be accepted while this
+hook is running!
+
+Arguments this hook receives are:
+
+  my ($self,$transaction,%args) = @_;
+  # %args is:
+  # %args = ( remote_ip    => inet_ntoa($iaddr),
+  #           remote_port  => $port,
+  #           local_ip     => inet_ntoa($laddr),
+  #           local_port   => $lport,
+  #           max_conn_ip  => $MAXCONNIP,
+  #           child_addrs  => [values %childstatus],
+  #         );
+
+B<NOTE:> the C<$transaction> is of course C<undef> at this time.
+
+Allowed return codes are
+
+=over 4
+
+=item DENY / DENY_DISCONNECT
+
+returns a B<550> to the client and ends the connection
+
+=item DENYSOFT / DENYSOFT_DISCONNECT
+
+returns a B<451> to the client and ends the connection
+
+=back
+
+Anything else is ignored.
+
+Example plugins are F<hosts_allow> and F<connection_time>. 
+
+=head2 hook_connect
+
+It is called at the start of a connection before the greeting is sent to 
+the connecting client.
+
+Arguments for this hook are
+
+  my $self = shift;
+
+B<NOTE:> in fact you get passed two more arguments, which are C<undef> at this
+early stage of the connection, so ignore them.
+
+Allowed return codes are
+
+=over 4
+
+=item OK
+
+Stop processing plugins, give the default response
+
+=item DECLINED
+
+Process the next plugin
+
+=item DONE
+
+Stop processing plugins and dont give the default response, i.e. the plugin 
+gave the response
+
+=item DENY
+
+Return hard failure code and disconnect
+
+=item DENYSOFT
+
+Return soft failure code and disconnect
+
+=back
+
+Example plugin for this hook is the F<check_relay> plugin. 
+
+=head2 hook_helo / hook_ehlo
+
+It is called after the client sent B<EHLO> (hook_ehlo) or B<HELO> (hook_helo)
+Allowed return codes are
+
+=over 4
+
+=item DENY
+
+Return a 550 code
+
+=item DENYSOFT
+
+Return a B<450> code
+
+=item DENY_DISCONNECT / DENYSOFT_DISCONNECT
+
+as above but with disconnect
+
+=item DONE
+
+Qpsmtpd wont do anything, the plugin sent the message
+
+=item DECLINED 
+
+Qpsmtpd will send the standard B<EHLO>/B<HELO> answer, of course only 
+if all plugins hooking I<helo/ehlo> return I<DECLINED>.
+
+=back
+
+Arguments of this hook are
+
+  my ($self, $transaction, $host) = @_;
+  # $host: the name the client sent in the 
+  # (EH|HE)LO line
+
+B<NOTE:> C<$transaction> is C<undef> at this point.
+
+=head2 hook_mail_pre
+
+After the B<MAIL FROM: > line sent by the client is broken into 
+pieces by the C<hook_mail_parse()>, this hook recieves the results.
+This hook may be used to pre-accept adresses without the surrounding 
+I<E<lt>E<gt>> (by adding them) or addresses like 
+I<E<lt>[EMAIL PROTECTED]<gt>> or I<E<lt>[EMAIL PROTECTED] E<gt>> by 
+removing the trailing I<"."> / C<" ">.
+
+Expected return values are I<OK> and an address which must be parseable
+by C<Qpsmtpd::Address-E<gt>parse()> on success or any other constant to
+indicate failure.
+
+Arguments are 
+
+  my ($self, $transaction, $addr) = @_;
+
+=head2 hook_mail
+
+Called right after the envelope sender line is parsed (the B<MAIL FROM: >
+command). The plugin gets passed a C<Qpsmtpd::Address> object, which means 
+the parsing and verifying the syntax of the address (and just the syntax, 
+no other checks) is already done. Default is to allow the sender address. 
+The remaining arguments are the extensions defined in RFC 1869 (if sent by 
+the client).
+
+B<NOTE:> According to the SMTP protocol, you can not reject an invalid
+sender until after the B<RCPT> stage (except for protocol errors, i.e.
+syntax errors in address). So store it in an C<$transaction-E<gt>note()> and 
+process it later in an rcpt hook.
+
+Allowed return codes are
+
+=over 4
+
+=item OK
+
+sender allowed
+
+=item DENY
+
+Return a hard failure code
+
+=item DENYSOFT
+
+Return a soft failure code
+
+=item DENY_DISCONNECT / DENYSOFT_DISCONNECT
+
+as above but with disconnect
+
+=item DECLINED
+
+next plugin (if any)
+
+=item DONE
+
+skip further processing, plugin sent response
+
+=back
+
+Arguments for this hook are 
+
+  my ($self,$transaction, $sender, %args) = @_;
+  # $sender: an Qpsmtpd::Address object for 
+  # sender of the message
+
+Example plugins for the C<hook_mail> are F<require_resolvable_fromhost>
+and F<check_badmailfrom>.
+
+=head2 hook_rcpt_pre
+
+See C<hook_mail_pre>, s/MAIL FROM:/RCPT TO:/.
+
+=head2 hook_rcpt
+
+This hook is called after the client sent an I<RCPT TO: > command (after 
+parsing the line). The given argument is parsed by C<Qpsmtpd::Address>, 
+then this hook is called. Default is to deny the mail with a soft error 
+code. The remaining arguments are the extensions defined in RFC 1869
+(if sent by the client).
+
+Allowed return codes
+
+=over 4
+
+=item OK
+
+recipient allowed
+
+=item DENY
+
+Return a hard failure code, for example for an I<User does not exist here> 
+message.
+
+=item DENYSOFT
+
+Return a soft failure code, for example if the connect to a user lookup 
+database failed
+
+=item DENY_DISCONNECT / DENYSOFT_DISCONNECT
+
+as above but with disconnect
+
+=item DONE
+
+skip further processing, plugin sent response
+
+=back
+
+Arguments are
+
+  my ($self, $transaction, $recipient, %args) = @_;
+  # $rcpt = Qpsmtpd::Address object with 
+  # the given recipient address
+
+Example plugin is F<rcpt_ok>.
+
+=head2 hook_data
+
+After the client sent the B<DATA> command, before any data of the message
+was sent, this hook is called. 
+
+B<NOTE:> This hook, like B<EHLO>, B<VRFY>, B<QUIT>, B<NOOP>, is an 
+endpoint of a pipelined command group (see RFC 1854) and may be used to 
+detect ``early talkers''. Since svn revision 758 the F<check_earlytalker>
+plugin may be configured to check at this hook for ``early talkers''.
+
+Allowed return codes are
+
+=over 4
+
+=item DENY
+
+Return a hard failure code
+
+=item DENYSOFT
+
+Return a soft failure code
+
+=item DENY_DISCONNECT / DENYSOFT_DISCONNECT
+
+as above but with disconnect
+
+=item DONE
+
+Plugin took care of receiving data and calling the queue (not recommended)
+
+B<NOTE:> The only real use for I<DONE> is implementing other ways of
+receiving the message, than the default... for example the CHUNKING SMTP
+extension (RFC 1869, 1830/3030) ... a plugin for this exists at 
+http://svn.perl.org/qpsmtpd/contrib/vetinari/experimental/chunking, but it
+was never tested ``in the wild''.
+
+=back
+
+Arguments:
+
+  my ($self, $transaction) = @_;
+
+Example plugin is F<greylisting>.
+
+=head2 hook_data_post
+
+The C<data_post> hook is called after the client sent the final C<.\r\n> 
+of a message, before the mail is sent to the queue.
+
+Allowed return codes are
+
+=over 4
+
+=item DENY
+
+Return a hard failure code
+
+=item DENYSOFT
+
+Return a soft failure code
+
+=item DENY_DISCONNECT / DENYSOFT_DISCONNECT
+
+as above but with disconnect
+
+=item DONE
+
+skip further processing (message will not be queued), plugin gave the response.
+
+B<NOTE:> just returning I<OK> from a special queue plugin does (nearly) 
+the same (i.e. dropping the mail to F</dev/null>) and you don't have to 
+send the response on your own.
+
+If you want the mail to be queued, you have to queue it manually!
+
+=back
+
+Arguments:
+
+  my ($self, $transaction) = @_;
+
+Example plugins: F<spamassassin>, F<virus/clamdscan>
+
+=head2 hook_queue_pre
+
+This hook is run, just before the mail is queued to the ``backend''. You 
+may modify the in-process transaction object (e.g. adding headers) or add 
+something like a footer to the mail (the latter is not recommended).
+
+Allowed return codes are 
+
+=over 4
+
+=item DONE
+
+no queuing is done
+
+=item OK / DECLINED
+
+queue the mail
+
+=back
+
+=head2 hook_queue
+
+When all C<data_post> hooks accepted the message, this hook is called. It 
+is used to queue the message to the ``backend''.
+
+Allowed return codes:
+
+=over 4
+
+=item DONE
+
+skip further processing (plugin gave response code)
+
+=item OK
+
+Return success message, i.e. tell the client the message was queued (this
+may be used to drop the message silently).
+
+=item DENY
+
+Return hard failure code
+
+=item DENYSOFT
+
+Return soft failure code, i.e. if disk full or other temporary queuing 
+problems
+
+=back
+
+Arguments:
+
+  my ($self, $transaction) = @_;
+
+Example plugins: all F<queue/*> plugins
+
+=head2 hook_queue_post
+
+This hook is called always after C<hook_queue>. If the return code is 
+B<not> I<OK>, a message (all remaining return values) with level I<LOGERROR> 
+is written to the log.
+Arguments are
+
+ my $self = shift;
+ 
+B<NOTE:> C<$transaction> is not valid at this point, therefore not mentioned.
+
+
+=head2 hook_reset_transaction
+
+This hook will be called several times. At the beginning of a transaction 
+(i.e. when the client sends a B<MAIL FROM:> command the first time), 
+after queueing the mail and every time a client sends a B<RSET> command.
+Arguments are 
+
+ my ($self, $transaction) = @_;
+
+B<NOTE:> don't rely on C<$transaction> being valid at this point.
+
+=head2 hook_quit
+
+After the client sent a B<QUIT> command, this hook is called (before the
+C<hook_disconnect>).
+
+Allowed return codes
+
+=over 4
+
+=item DONE
+
+plugin sent response
+
+=item DECLINED
+
+next plugin and / or qpsmtpd sends response
+
+=back
+
+Arguments: the only argument is C<$self>
+
+=cut 
+
+### XXX: FIXME pass the rest of the line to this hook?
+
+=pod
+
+Expample plugin is the F<quit_fortune> plugin.
+
+=head2 hook_disconnect
+
+This hook will be called from several places: After a plugin returned 
+I<DENY(|SOFT)_DISCONNECT>, before connection is disconnected or after the 
+client sent the B<QUIT> command, AFTER the quit hook and ONLY if no plugin 
+hooking C<hook_quit> returned I<DONE>.
+
+All return values are ignored, arguments are just C<$self>
+
+Example plugin is F<logging/file>
+
+=head2 hook_post_connection
+
+This is the counter part of the C<pre-connection> hook, it is called 
+directly before the connection is finished, for example, just before the 
+qpsmtpd-forkserver instance exits or if the client drops the connection 
+without notice (without a B<QUIT>). This hook is not called if the qpsmtpd
+instance is killed.
+
+=cut 
+
+FIXME: we should run this hook on a ``SIGHUP'' or some other signal?
+
+=pod 
+
+B<NOTE:> This hook only works in the (x)inetd, -forkserver and -prefork 
+flavours.
+The only argument is C<$self> and all return codes are ignored, it would 
+be too late anyway :-).
+
+Example: F<connection_time>
+
+=head1 Parsing Hooks
+
+Before the line from the client is parsed by 
+C<Qpsmtpd::Command-E<gt>parse()> with the built in parser, these hooks 
+are called. They can be used to supply a parsing function for the line,
+which will be used instead of the built in parser.
+
+The hook must return two arguments, the first is (currently) ignored,
+the second argument must be a (CODE) reference to a sub routine. This sub 
+routine receives three arguments:
+
+=over 4
+
+=item $self
+
+the plugin object
+
+=item $cmd
+
+the command (i.e. the first word of the line) sent by the client
+
+=item $line
+
+the line sent by the client without the first word
+
+=back
+
+Expected return values from this sub are I<DENY> and a reason which is 
+sent to the client or I<OK> and the C<$line> broken into pieces according
+to the syntax rules for the command.
+
+B<NOTE: ignore the example from C<Qpsmtpd::Command>, the 
C<unrecognized_command_parse> hook was never implemented,...>
+
+=head2 hook_helo_parse / hook_ehlo_parse
+
+The provided sub routine must return two or more values. The first is 
+discarded, the second is the hostname (sent by the client as argument 
+to the B<HELO> / B<EHLO> command). All other values are passed to the 
+helo / ehlo hook. This hook may be used to change the hostname the client
+sent... not recommended, but if your local policy says only to accept
+I<HELO> hosts with FQDNs and you have a legal client which can not be 
+changed to send his FQDN, this is the right place.
+
+=head2 hook_mail_parse / hook_rcpt_parse
+
+The provided sub routine must return two or more values. The first is 
+either I<OK> to indicate that parsing of the line was successfull
+or anything else to bail out with I<501 Syntax error in command>. In
+case of failure the second argument is used as the error message for the 
+client.
+
+If parsing was successfull, the second argument is the sender's / 
+recipient's address (this may be without the surrounding I<E<lt>> and 
+I<E<gt>>, don't add them here, use the C<hook_mail_pre()> / 
+C<hook_rcpt_pre()> methods for this). All other arguments are 
+sent to the C<mail / rcpt> hook as B<MAIL> / B<RCPT> parameters (see 
+RFC 1869 I<SMTP Service Extensions> for more info). Note that 
+the mail and rcpt hooks expect a list of key/value pairs as the 
+last arguments.
+
+=head2 hook_auth_parse
+
+B<FIXME...>
+
+=head1 Special hooks
+
+Now some special hooks follow. Some of these hooks are some internal hooks, 
+which may be used to alter the logging or retrieving config values from 
+other sources (other than flat files) like SQL databases.
+
+=head2 hook_logging
+
+This hook is called when a log message is written, for example in a plugin 
+it fires if someone calls C<$self-E<gt>log($level, $msg);>. Allowed
+return codes are
+
+=over 4
+
+=item DECLINED
+
+next logging plugin
+
+=item OK
+
+(not I<DONE>, as some might expect!) ok, plugin logged the message
+
+=back
+
+Arguments are
+
+  my ($self, $transaction, $trace, $hook, $plugin, @log) = @_;
+  # $trace: level of message, for example 
+  #          LOGWARN, LOGDEBUG, ...
+  # $hook:  the hook in\/for which this logging 
+  #          was called
+  # $plugin: the plugin calling this hook
+  # @log:   the log message
+
+B<NOTE:> C<$transaction> may be C<undef>, depending when / where this hook
+is called. It's probably best not to try acessing it.
+
+All F<logging/*> plugins can be used as example plugins.
+
+=head2 hook_deny
+
+This hook is called after a plugin returned I<DENY>, I<DENYSOFT>, 
+I<DENY_DISCONNECT> or I<DENYSOFT_DISCONNECT>. All return codes are ignored, 
+arguments are
+
+  my ($self, $transaction, $prev_hook, $return, $return_text) = @_;
+
+B<NOTE:> C<$transaction> may be C<undef>, depending when / where this hook
+is called. It's probably best not to try acessing it.
+
+Example plugin for this hook is F<logging/adaptive>.
+
+=head2 hook_ok
+
+The counter part of C<hook_deny>, it is called after a plugin B<did not>
+return I<DENY>, I<DENYSOFT>, I<DENY_DISCONNECT> or I<DENYSOFT_DISCONNECT>. 
+All return codes are ignored, arguments are
+
+  my ( $self, $transaction, $prev_hook, $return, $return_text ) = @_;
+
+B<NOTE:> C<$transaction> may be C<undef>, depending when / where this hook
+is called. It's probably best not to try acessing it.
+
+=head2 hook_config
+
+Called when a config file is requested, for example in a plugin it fires 
+if someone calls C<my @cfg = $self-E<gt>qp-E<gt>config($cfg_name);>. 
+Allowed return codes are
+
+=over 4
+
+=item DECLINED
+
+plugin didn't find the requested value
+
+=item OK
+
+requested values as C<@list>, example:
+
+  return (OK, @{$config{$value}}) 
+    if exists $config{$value};
+  return (DECLINED);
+
+=back
+
+Arguments:
+
+  my ($self,$transaction,$value) = @_; 
+  # $value: the requested config item(s)
+
+B<NOTE:> C<$transaction> may be C<undef>, depending when / where this hook
+is called. It's probably best not to try acessing it.
+
+Example plugin is F<http_config> from the qpsmtpd distribution.
+
+=head2 hook_unrecognized_command
+
+This is called if the client sent a command unknown to the core of qpsmtpd.
+This can be used to implement new SMTP commands or just count the number 
+of unknown commands from the client, see below for examples.
+Allowed return codes:
+
+=over 4
+
+=item DENY_DISCONNECT
+
+Return B<521> and disconnect the client
+
+=item DENY
+
+Return B<500>
+
+=item DONE
+
+Qpsmtpd wont do anything; the plugin responded, this is what you want to 
+return, if you are implementing new commands
+
+=item Anything else...
+
+Return B<500 Unrecognized command>
+
+=back
+
+Arguments:
+
+  my ($self, $transaction, $cmd, @args) = @_;
+  # $cmd  = the first "word" of the line 
+  #         sent by the client
+  # @args = all the other "words" of the 
+  #         line sent by the client
+  #         "word(s)": white space split() line
+
+B<NOTE:> C<$transaction> may be C<undef>, depending when / where this hook
+is called. It's probably best not to try acessing it.
+
+Example plugin is F<tls>.
+
+=head2 hook_vrfy
+
+If the client sents the B<VRFY> command, this hook is called. Default is to 
+return a message telling the user to just try sending the message.
+Allowed return codes:
+
+=over 4
+
+=item OK
+
+Recipient Exists
+
+=item DENY
+
+Return a hard failure code
+
+=item DONE
+
+Return nothing and move on
+
+=item Anything Else...
+
+Return a B<252>
+
+=back
+
+Arguments are:
+
+ my ($self) = shift;
+
+=cut 
+
+FIXME: this sould be changed in Qpsmtpd::SMTP to pass the rest of the line
+as arguments to the hook
+
+=pod
+
+=head2 hook_post_fork
+
+B<NOTE:> This hook is only available in qpsmtpd-async.
+
+It is called while starting qpsmtpd-async. You can run more than one 
+instance of qpsmtpd-async (one per CPU probably). This hook is called 
+after forking one instance.
+
+Arguments: 
+
+ my $self = shift;
+
+The return values of this hook are discarded.
+
+=head1 Authentication hooks
+
+=cut 
+
+B<FIXME missing:> auth_parse
+
+#=head2 auth
+
+B<FIXME>
+
+#=head2 auth-plain
+
+B<FIXME>
+
+#=head2 auth-login
+
+B<FIXME>
+
+#=head2 auth-cram-md5
+
+B<FIXME>
+
+=pod
+
+...documentation will follow later
+
+=head1 Writing your own plugins
+
+This is a walk through a new queue plugin, which queues the mail to a (remote)
+QMQP-Server.
+
+First step is to pull in the necessary modules
+
+ use IO::Socket;
+ use Text::Netstring qw( netstring_encode 
+                         netstring_decode 
+                         netstring_verify 
+                         netstring_read );
+
+We know, we need a server to send the mails to. This will be the same 
+for every mail, so we can use arguments to the plugin to configure this 
+server (and port).
+
+Inserting this static config is done in C<register()>:
+
+  sub register {
+    my ($self, $qp, @args) = @_;
+  
+    die "No QMQP server specified in qmqp-forward config"
+      unless @args;
+  
+    $self->{_qmqp_timeout} = 120;
+  
+    if ($args[0] =~ /^([\.\w_-]+)$/) {
+      $self->{_qmqp_server} = $1;
+    }
+    else {
+      die "Bad data in qmqp server: $args[0]";
+    }
+  
+    $self->{_qmqp_port} = 628;
+    if (@args > 1 and $args[1] =~ /^(\d+)$/) {
+      $self->{_qmqp_port} = $1;
+    }
+  
+    $self->log(LOGWARN, "WARNING: Ignoring additional arguments.") 
+      if (@args > 2);
+  }
+
+We're going to write a queue plugin, so we need to hook to the I<queue>
+hook.
+
+  sub hook_queue {
+    my ($self, $transaction) = @_;
+  
+    $self->log(LOGINFO, "forwarding to $self->{_qmqp_server}:"
+                       ."$self->{_qmqp_port}");
+
+The first step is to open a connection to the remote server.
+
+   my $sock = IO::Socket::INET->new(
+                 PeerAddr => $self->{_qmqp_server},
+                 PeerPort => $self->{_qmqp_port},
+                 Timeout  => $self->{_qmqp_timeout},
+                 Proto    => 'tcp')
+      or $self->log(LOGERROR, "Failed to connect to "
+                      ."$self->{_qmqp_server}:"
+                      ."$self->{_qmqp_port}: $!"), 
+        return(DECLINED);
+    $sock->autoflush(1);
+
+=over 4
+
+=item *
+
+The client starts with a safe 8-bit text message. It encodes the message 
+as the byte string C<firstline\012secondline\012 ... \012lastline>. (The
+last line is usually, but not necessarily, empty.) The client then encodes
+this byte string as a netstring. The client also encodes the envelope 
+sender address as a netstring, and encodes each envelope recipient address
+as a netstring.
+
+The client concatenates all these netstrings, encodes the concatenation 
+as a netstring, and sends the result. 
+
+(from L<http://cr.yp.to/proto/qmqp.html>)
+
+=back
+
+The first idea is to build the package we send, in the order described 
+in the paragraph above:
+
+  my $message = $transaction->header->as_string;
+  $transaction->body_resetpos;
+  while (my $line = $transaction->body_getline) {
+    $message .= $line;
+  }
+  $message  = netstring_encode($message);
+  $message .= netstring_encode($transaction->sender->address);
+  for ($transaction->recipients) {
+    push @rcpt, $_->address;
+  }
+  $message .= join "", netstring_encode(@rcpt);
+  print $sock netstring_encode($message)
+    or do { 
+      my $err = $!;
+      $self->_disconnect($sock);
+      return(DECLINED, "Failed to print to socket: $err");
+    };
+
+This would mean, we have to hold the full message in memory... Not good 
+for large messages, and probably even slower (for large messages).
+
+Luckily it's easy to build a netstring without the help of the
+C<Text::Netstring> module if you know the size of the string (for more
+info about netstrings see L<http://cr.yp.to/proto/netstrings.txt>).
+
+We start with the sender and recipient addresses:
+
+  my ($addrs, $headers, @rcpt);
+  $addrs = netstring_encode($transaction->sender->address);
+  for ($transaction->recipients) {
+    push @rcpt, $_->address;
+  }
+  $addrs .= join "", netstring_encode(@rcpt);
+
+Ok, we got the sender and the recipients, now let's see what size the
+message is.
+
+  $headers   = $transaction->header->as_string;
+  my $msglen = length($headers) + $transaction->body_length;
+
+We've got everything we need. Now build the netstrings for the full package
+and the message.
+
+First the beginning of the netstring of the full package 
+
+  # (+ 2: the ":" and "," of the message's netstring)
+  print $sock ($msglen + length($msglen) + 2 + length($addrs))
+               .":"
+               ."$msglen:$headers" ### beginning of messages netstring
+    or do { 
+      my $err = $!;
+      $self->_disconnect($sock);
+      return(DECLINED, 
+             "Failed to print to socket: $err");
+    };
+
+Go to beginning of the body
+
+  $transaction->body_resetpos;
+
+If the message is spooled to disk, read the message in
+blocks and write them to the server
+
+  if ($transaction->body_fh) {
+    my $buff;
+    my $size = read $transaction->body_fh, $buff, 4096;
+    unless (defined $size) {
+      my $err = $!;
+      $self->_disconnect($sock);
+      return(DECLINED, "Failed to read from body_fh: $err");
+    }
+    while ($size) {
+      print $sock $buff
+        or do { 
+          my $err = $!;
+          $self->_disconnect($sock);
+          return(DECLINED, "Failed to print to socket: $err");
+        };
+  
+      $size = read $transaction->body_fh, $buff, 4096;
+      unless (defined $size) {
+        my $err = $!;
+        $self->_disconnect($sock);
+        return(DECLINED, 
+               "Failed to read from body_fh: $err");
+      }
+    }
+  }
+
+Else we have to read it line by line ...
+
+  else { 
+    while (my $line = $transaction->body_getline) {
+      print $sock $line
+        or do { 
+          my $err = $!;
+          $self->_disconnect($sock);
+          return(DECLINED, "Failed to print to socket: $err");
+        };
+    }
+  }
+
+Message is at the server, now finish the package.
+
+  print $sock ","    # end of messages netstring
+             .$addrs # sender + recpients
+             .","    # end of netstring of 
+                     #   the full package
+    or do { 
+      my $err = $!;
+      $self->_disconnect($sock);
+      return(DECLINED, 
+             "Failed to print to socket: $err");
+    };
+
+We're done. Now let's see what the remote qmqpd says...
+
+
+=over 4 
+
+=item *
+
+(continued from L<http://cr.yp.to/proto/qmqp.html>:)
+
+The server's response is a nonempty string of 8-bit bytes, encoded as a 
+netstring.
+  
+The first byte of the string is either K, Z, or D. K means that the 
+message has been accepted for delivery to all envelope recipients. This 
+is morally equivalent to the 250 response to DATA in SMTP; it is subject 
+to the reliability requirements of RFC 1123, section 5.3.3. Z means 
+temporary failure; the client should try again later. D means permanent 
+failure.
+   
+Note that there is only one response for the entire message; the server 
+cannot accept some recipients while rejecting others.
+
+=back
+
+
+    my $answer = netstring_read($sock);
+    $self->_disconnect($sock);
+    
+    if (defined $answer and netstring_verify($answer)) {
+      $answer = netstring_decode($answer);
+  
+      $answer =~ s/^K// and return(OK,
+                                 "Queued! $answer");
+      $answer =~ s/^Z// and return(DENYSOFT, 
+                                 "Deferred: $answer");
+      $answer =~ s/^D// and return(DENY,
+                                 "Denied: $answer");
+    }
+
+If this is the only F<queue/*> plugin, the client will get a 451 temp error:
+
+    return(DECLINED, "Protocol error");
+  }
+
+  sub _disconnect {
+    my ($self,$sock) = @_;
+    if (defined $sock) {
+      eval { close $sock; };
+      undef $sock;
+    }
+  }
+
+=head1 Advanced Playground
+
+=head2 Discarding messages
+
+If you want to make the client think a message has been regularily accepted,
+but in real you delete it or send it to F</dev/null>, ..., use something
+like the following plugin and load it before your default queue plugin.
+
+  sub hook_queue {
+    my ($self, $transaction) = @_;
+    if ($transaction->notes('discard_mail')) {
+      my $msg_id = $transaction->header->get('Message-Id') || '';
+      $msg_id =~ s/[\r\n].*//s;
+      return(OK, "Queued! $msg_id");
+    }
+    return(DECLINED);
+  }
+
+=head2 TBC... :-)
+
+=cut
+
+# vim: ts=2 sw=2 expandtab

Reply via email to