Merge pull request #117 from msimerson/docs-to-md

docs/* from POD -> Markdown
This commit is contained in:
Jared Johnson 2014-09-18 11:37:11 -05:00
commit 92fadd703a
19 changed files with 2090 additions and 2656 deletions

View File

@ -25,14 +25,13 @@ config.sample/smtpauth-checkpassword
config.sample/tls_before_auth config.sample/tls_before_auth
config.sample/tls_ciphers config.sample/tls_ciphers
CREDITS CREDITS
docs/advanced.pod docs/advanced.md
docs/authentication.pod docs/authentication.md
docs/config.pod docs/config.md
docs/development.pod docs/development.md
docs/hooks.pod docs/hooks.md
docs/logging.pod docs/logging.md
docs/plugins.pod docs/writing.md
docs/writing.pod
lib/Apache/Qpsmtpd.pm lib/Apache/Qpsmtpd.pm
lib/Qpsmtpd.pm lib/Qpsmtpd.pm
lib/Qpsmtpd/Address.pm lib/Qpsmtpd/Address.pm
@ -147,9 +146,8 @@ plugins/whitelist
qpsmtpd qpsmtpd
qpsmtpd-forkserver qpsmtpd-forkserver
qpsmtpd-prefork qpsmtpd-prefork
README
README.md README.md
README.plugins README.plugins.md
run.forkserver run.forkserver
run.tcpserver run.tcpserver
STATUS STATUS
@ -207,6 +205,6 @@ t/qpsmtpd.t
t/rset.t t/rset.t
t/Test/Qpsmtpd.pm t/Test/Qpsmtpd.pm
t/Test/Qpsmtpd/Plugin.pm t/Test/Qpsmtpd/Plugin.pm
UPGRADING.pod UPGRADING.md
xt/01-syntax.t xt/01-syntax.t
xt/02-pod.t xt/02-pod.t

199
README
View File

@ -1,199 +0,0 @@
#
# this file is best read with `perldoc README`
#
=head1 NAME
Qpsmtpd - qmail perl simple mail transfer protocol daemon
web:
http://smtpd.github.io/qpsmtpd/
mailinglist:
qpsmtpd-subscribe@perl.org
FAQ:
https://github.com/smtpd/qpsmtpd/wiki/faq
=head1 DESCRIPTION
What is Qpsmtpd?
Qpsmtpd is an extensible SMTP engine written in Perl. No, make that
easily extensible! See plugins/quit_fortune for a very useful, er,
cute example.
=head2 License
Qpsmtpd is licensed under the MIT License; see the LICENSE file for
more information.
=head2 What's new in this release?
See the Changes file! :-)
=head1 Installation
=head2 Required Perl Modules
The following Perl modules are required:
Net::DNS
MIME::Base64
Mail::Header (part of the MailTools distribution)
If you use a version of Perl older than 5.8.0 you will also need
Data::Dumper
File::Temp
Time::HiRes
The easiest way to install modules from CPAN is with the CPAN shell.
Run it with
perl -MCPAN -e shell
=head2 qpsmtpd installation
Make a new user and a directory where you'll install qpsmtpd. I
usually use "smtpd" for the user and /home/smtpd/qpsmtpd/ for the
directory.
Put the files there. If you install from git you can just do
run the following command in the /home/smtpd/ directory.
git clone git://github.com/smtpd/qpsmtpd.git
Beware that the master branch might be unstable and unsuitable for anything
but development, so you might want to get a specific release, for
example (after running git clone):
git checkout -b local_branch v0.93
chmod o+t ~smtpd/qpsmtpd/ (or whatever directory you installed qpsmtpd
in) to make supervise start the log process.
Edit the file config/IP and put the ip address you want to use for
qpsmtpd on the first line (or use 0 to bind to all interfaces).
If you use the supervise tools, then you are practically done!
Just symlink /home/smtpd/qpsmtpd into your /services (or /var/services
or /var/svscan or whatever) directory. Remember to shutdown
qmail-smtpd if you are replacing it with qpsmtpd.
If you don't use supervise, then you need to run the ./run script in
some other way.
The smtpd user needs write access to ~smtpd/qpsmtpd/tmp/ but should
not need to write anywhere else. This directory can be configured
with the "spool_dir" configuration and permissions can be set with
"spool_perms".
As per version 0.25 the distributed ./run script runs tcpserver with
the -R flag to disable identd lookups. Remove the -R flag if that's
not what you want.
=head2 Configuration
Configuration files can go into either /var/qmail/control or into the
config subdirectory of the qpsmtpd installation. Configuration should
be compatible with qmail-smtpd making qpsmtpd a drop-in replacement.
If qmail is installed in a nonstandard location you should set the
$QMAIL environment variable to that location in your "./run" file.
If there is anything missing, then please send a patch (or just
information about what's missing) to the mailinglist or a PR to github.
=head1 Better Performance
For better performance we recommend using "qpsmtpd-forkserver" or
running qpsmtpd under Apache 2.x. If you need extremely high
concurrency use http://haraka.github.io/
=head1 Plugins
The qpsmtpd core only implements the SMTP protocol. 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 "hooks" provided by the qpsmtpd core engine.
At least one plugin MUST allow or deny the RCPT command to enable
receiving mail. The "rcpt_ok" is one basic plugin that does
this. Other plugins provide extra functionality related to this; for
example the resolvable_fromhost plugin described above.
=head1 Configuration files
All the files used by qmail-smtpd should be supported; so see the man
page for qmail-smtpd. Extra files used by qpsmtpd include:
=over 4
=item plugins
List of plugins, one per line, to be loaded in the order they
appear in the file. Plugins are in the plugins directory (or in
a subdirectory of there).
=item rhsbl_zones
Right hand side blocking lists, one per line. For example:
dsn.rfc-ignorant.org does not accept bounces - http://www.rfc-ignorant.org/
See http://www.rfc-ignorant.org/ for more examples.
=item dnsbl_zones
Normal ip based DNS blocking lists ("RBLs"). For example:
relays.ordb.org
spamsources.fabel.dk
=item spool_dir
If this file contains a directory, it will be the spool directory
smtpd uses during the data transactions. If this file doesn't exist, it
will default to use $ENV{HOME}/tmp/. This directory should be set with
a mode of 700 and owned by the smtpd user.
=item spool_perms
The default spool permissions are 0700. If you need some other value,
chmod the directory and set it's octal value in config/spool_perms.
=item tls_before_auth
If this file contains anything except a 0 on the first noncomment line, then
AUTH will not be offered unless TLS/SSL are in place, either with STARTTLS,
or SMTP-SSL on port 465.
=item everything (?) that qmail-smtpd supports.
In my test qpsmtpd installation I have a "config/me" file containing
the hostname I use for testing qpsmtpd (so it doesn't introduce itself
with the normal name of the server).
=back
=head1 Problems
In case of problems, always check the logfile first.
By default, qpsmtpd logs to log/main/current. Qpsmtpd can log a lot of
debug information. You can get more or less by adjusting the number in
config/loglevel. Between 1 and 3 should give you a little. Setting it
to 10 or higher will get lots of information in the logs.
If the logfile doesn't give away the problem, then post to the
mailinglist (subscription instructions above). If possible, put
the logfile on a webserver and include a reference to it in the mail.

View File

@ -1,13 +0,0 @@
#
# read this with 'perldoc README.plugins' ...
#
=head1 qpsmtpd plugin system; developer documentation
Plugin documentation is now in F<docs/plugins.pod>.
See the examples in plugins/ and ask questions on the qpsmtpd
mailinglist; subscribe by sending mail to qpsmtpd-subscribe@perl.org.
=cut

333
README.plugins.md Normal file
View File

@ -0,0 +1,333 @@
# 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 _hooks_ provided by the qpsmtpd core engine.
At least one plugin __must__ allow or deny the __RCPT__ command to enable
receiving mail. The `check_relay` plugin is the standard plugin for this.
Other plugins provide extra functionality related to this; for example the
`resolvable_fromhost` plugin.
## Loading Plugins
The list of plugins to load are configured in the _config/plugins_
configuration file. One plugin per line, empty lines and lines starting
with _#_ are ignored. The order they are loaded is the same as given
in this config file. This is also the order the registered _hooks_
are run. The plugins are loaded from the `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
`virus/clamdscan` in the example below. Alternate plugin directories
may be given in the `config/plugin_dirs` config file, one directory
per line, these will be searched first before using the builtin fallback
of `plugins/` relative to the qpsmtpd root directory. It may be
necessary, that the `config/plugin_dirs` must be used (if you're using
`Apache::Qpsmtpd`, for example).
Some plugins may be configured by passing arguments in the `plugins`
config file.
A plugin can be loaded two or more times with different arguments by adding
_:N_ to the plugin filename, with _N_ being a number, usually starting at
_0_.
Another method to load a plugin is to create a valid perl module, drop this
module in perl's `@INC` path and give the name of this module as
plugin name. The only restriction to this is, that the module name __must__
contain _::_, e.g. `My::Plugin` would be ok, `MyPlugin` not. Appending of
_:0_, _: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
# Anatomy of a plugin
A plugin has at least one method, which inherits from the
`Qpsmtpd::Plugin` object. The first argument for this method is always the
plugin object itself (and usually called `$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.
## Initialisation
After the plugin is loaded the `init()` method of the plugin is called,
if present. The arguments passed to `init()` are
- $self
the current plugin object, usually called `$self`
- $qp
the Qpsmtpd object, usually called `$qp`.
- @args
the values following the plugin name in the `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, ...
This is mainly used for inheriting from other plugins, but may be used to do
the same as in `register()`.
The next step is to register the hooks the plugin provides. Any method which
is named `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 `s/\W/_/g;`, see
["Hook - Subroutine translations"](#hook-subroutine-translations) for more info. If you choose
not to use the default naming convention, you need to register the hooks in
your plugin in the `register()` method (see below) with the
`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 `register()` method is called last. It receives the same arguments as
`init()`. There is no restriction, what you can do in `register()`, but
creating database connections and reuse them later in the process may not be
a good idea. This initialisation happens before any `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 the pperl flavor but
not for `qpsmtpd` started by (x)inetd or tcpserver.
In short: don't do it if you want to write portable plugins.
## Hook - Subroutine translations
As mentioned above, the hook name needs to be translated to a valid perl
`sub` name. This is done like
($sub = $hook) =~ s/\W/_/g;
$sub = "hook_$sub";
Some examples follow, for a complete list of available (documented ;-))
hooks (method names), use something like
$ perl -lne 'print if s/^=head2\s+(hook_\S+)/$1/' docs/plugins.pod
All valid hooks are defined in `lib/Qpsmtpd/Plugins.pm`, `our @hooks`.
### Translation table
hook method
---------- ------------
config hook_config
queue hook_queue
data hook_data
data_post hook_data_post
quit hook_quit
rcpt hook_rcpt
mail hook_mail
ehlo hook_ehlo
helo hook_helo
auth hook_auth
auth-plain hook_auth_plain
auth-login hook_auth_login
auth-cram-md5 hook_auth_cram_md5
connect hook_connect
reset_transaction hook_reset_transaction
unrecognized_command hook_unrecognized_command
## Inheritance
Inheriting methods from other plugins is an advanced topic. You can alter
arguments for the underlying plugin, prepare something for the _real_
plugin or skip a hook with this. Instead of modifying `@ISA`
directly in your plugin, use the `isa_plugin()` method from the
`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);
}
See also chapter `Changing return values` and
`contrib/vetinari/rcpt_ok_maxrelay` in SVN.
## Config files
Most of the existing plugins fetch their configuration data from files in the
`config/` sub directory. This data is read at runtime and may be changed
without restarting qpsmtpd.
__(FIXME: caching?!)__
The contents of the files can be fetched via
@lines = $self->qp->config("my_config");
All empty lines and lines starting with `#` 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 `config`
hook.
## Logging
Log messages can be written to the log file (or STDERR if you use the
`logging/warn` plugin) with
$self->log($loglevel, $logmessage);
The log level is one of (from low to high priority)
- LOGDEBUG
- LOGINFO
- LOGNOTICE
- LOGWARN
- LOGERROR
- LOGCRIT
- LOGALERT
- LOGEMERG
While debugging your plugins, set your plugins loglevel to LOGDEBUG. This
will log every logging statement within your plugin.
For more information about logging, see `docs/logging.pod`.
## Information about the current plugin
Each plugin inherits the public methods from `Qpsmtpd::Plugin`.
- plugin\_name()
Returns the name of the currently running plugin
- hook\_name()
Returns the name of the running hook
- auth\_user()
Returns the name of the user the client is authed as (if authentication is
used, of course)
- auth\_mechanism()
Returns the auth mechanism if authentication is used
- connection()
Returns the `Qpsmtpd::Connection` object associated with the current
connection
- transaction()
Returns the `Qpsmtpd::Transaction` object associated with the current
transaction
## 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.
- 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).
- 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 `$self->qp->temp_dir()`, which takes a
single parameter for the permissions requested (see [mkdir](https://metacpan.org/pod/mkdir) for details).
A directory created like this will not be deleted when the transaction
is ended.
- spool\_dir()
Returns the configured system-wide spool directory.
## 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 `count_unrecognized_commands` plugin from the qpsmtpd core
distribution.
The transaction note starts after the __MAIL FROM:__ command and are just
valid for the current transaction, see below in the `reset_transaction`
hook when the transaction ends.
# 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 _DECLINED_.
Plugins are run in the order they are listed in the `plugins`
configuration file.
The return constants are defined in `Qpsmtpd::Constants` and have
the following meanings:
- DECLINED
Plugin declined work; proceed as usual. This return code is _always allowed_
unless noted otherwise.
- OK
Action allowed.
- DENY
Action denied.
- DENYSOFT
Action denied; return a temporary rejection code (say __450__ instead
of __550__).
- 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.
- DENYSOFT\_DISCONNECT
Action denied; return a temporary rejection code and disconnect the client.
See note above about SMTP specs.
- DONE
Finishing processing of the request. Usually used when the plugin sent the
response to the client.

View File

@ -1,22 +1,21 @@
# Upgrade notes
=head1 Upgrade notes
When upgrading please review these notes for the versions you are When upgrading please review these notes for the versions you are
upgrading I<from>. upgrading _from_.
=head2 v0.84 or below ## v0.84 or below
=head3 CHECK_RELAY, CHECK_NORELAY, RELAY_ONLY ### CHECK\_RELAY, CHECK\_NORELAY, RELAY\_ONLY
All 3 plugins are deprecated and replaced with a new 'relay' All 3 plugins are deprecated and replaced with a new 'relay'
plugin. The new plugin reads the same config files (see 'perldoc plugin. The new plugin reads the same config files (see 'perldoc
plugins/relay') as the previous plugins. To get the equivalent plugins/relay') as the previous plugins. To get the equivalent
functionality of enabling 'relay_only', use the 'only' argument to the functionality of enabling 'relay\_only', use the 'only' argument to the
relay plugin as documented in the RELAY ONLY section of plugins/relay. relay plugin as documented in the RELAY ONLY section of plugins/relay.
=head3 GREYLISTING plugin ### GREYLISTING plugin
'mode' config argument is deprecated. Use reject and reject_type instead. 'mode' config argument is deprecated. Use reject and reject\_type instead.
The greylisting DB format has changed to accommodate IPv6 The greylisting DB format has changed to accommodate IPv6
addresses. (The DB key has colon ':' seperated fields, and IPv6 addresses. (The DB key has colon ':' seperated fields, and IPv6
@ -28,19 +27,16 @@ qpsmtpd once, make one connection. A log entry will be made, telling
how many records were upgraded. Remove the upgrade option from your how many records were upgraded. Remove the upgrade option from your
config. config.
=head3 SPF plugin ### SPF plugin
spf_deny setting deprecated. Use reject N setting instead, which spf\_deny setting deprecated. Use reject N setting instead, which
provides administrators with more granular control over SPF. For provides administrators with more granular control over SPF. For
backward compatibility, a spf_deny setting of 1 is mapped to 'reject backward compatibility, a spf\_deny setting of 1 is mapped to 'reject
3' and a 'spf_deny 2' is mapped to 'reject 4'. 3' and a 'spf\_deny 2' is mapped to 'reject 4'.
### P0F plugin
=head3 P0F plugin
defaults to p0f v3 (was v2). defaults to p0f v3 (was v2).
Upgrade p0f to version 3 or add 'version 2' to your p0f line in Upgrade p0f to version 3 or add 'version 2' to your p0f line in
config/plugins. perldoc plugins/ident/p0f for more details. config/plugins. perldoc plugins/ident/p0f for more details.

70
docs/advanced.md Normal file
View File

@ -0,0 +1,70 @@
# Advanced Playground
## 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 `/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);
}
## Changing return values
This is an example how to use the `isa_plugin` method.
The `rcpt_ok_maxrelay` plugin wraps the `rcpt_ok` plugin. The `rcpt_ok`
plugin checks the `rcpthosts` and `morercpthosts` config files for
domains, which we accept mail for. If not found it tells the
client that relaying is not allowed. Clients which are marked as
`relay clients` are excluded from this rule. This plugin counts the
number of unsuccessfull relaying attempts and drops the connection if
too many were made.
The optional parameter `MAX_RELAY_ATTEMPTS` configures this plugin to drop
the connection after `MAX_RELAY_ATTEMPTS` unsuccessful relaying attempts.
Set to `0` to disable, default is `5`.
Note: Do not load both (`rcpt_ok` and `rcpt_ok_maxrelay`). This plugin
should be configured to run `last`, like `rcpt_ok`.
use Qpsmtpd::DSN;
sub init {
my ($self, $qp, @args) = @_;
die "too many arguments"
if @args > 1;
$self->{_count_relay_max} = defined $args[0] ? $args[0] : 5;
$self->isa_plugin("rcpt_ok");
}
sub hook_rcpt {
my ($self, $transaction, $recipient) = @_;
my ($rc, @msg) = $self->SUPER::hook_rcpt($transaction, $recipient);
return ($rc, @msg)
unless (($rc == DENY) and $self->{_count_relay_max});
my $count =
($self->connection->notes('count_relay_attempts') || 0) + 1;
$self->connection->notes('count_relay_attempts', $count);
return ($rc, @msg) unless ($count > $self->{_count_relay_max});
return Qpsmtpd::DSN->relaying_denied(DENY_DISCONNECT,
"Too many relaying attempts");
}
## Results of other hooks
If we're in a transaction, the results of a callback are stored in
$self->transaction->notes($code->{name})->{"hook_$hook"}->{return}
If we're in a connection, store things in the connection notes instead.

View File

@ -1,96 +0,0 @@
#
# This file is best read with ``perldoc advanced.pod''
#
###
# Conventions:
# plugin names: F<myplugin>
# 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 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 Changing return values
This is an example how to use the C<isa_plugin> method.
The B<rcpt_ok_maxrelay> plugin wraps the B<rcpt_ok> plugin. The B<rcpt_ok>
plugin checks the F<rcpthosts> and F<morercpthosts> config files for
domains, which we accept mail for. If not found it tells the
client that relaying is not allowed. Clients which are marked as
C<relay clients> are excluded from this rule. This plugin counts the
number of unsuccessfull relaying attempts and drops the connection if
too many were made.
The optional parameter I<MAX_RELAY_ATTEMPTS> configures this plugin to drop
the connection after I<MAX_RELAY_ATTEMPTS> unsuccessful relaying attempts.
Set to C<0> to disable, default is C<5>.
Note: Do not load both (B<rcpt_ok> and B<rcpt_ok_maxrelay>). This plugin
should be configured to run I<last>, like B<rcpt_ok>.
use Qpsmtpd::DSN;
sub init {
my ($self, $qp, @args) = @_;
die "too many arguments"
if @args > 1;
$self->{_count_relay_max} = defined $args[0] ? $args[0] : 5;
$self->isa_plugin("rcpt_ok");
}
sub hook_rcpt {
my ($self, $transaction, $recipient) = @_;
my ($rc, @msg) = $self->SUPER::hook_rcpt($transaction, $recipient);
unless (($rc == DENY) and $self->{_count_relay_max}) {
return $rc, @msg;
};
my $count =
($self->connection->notes('count_relay_attempts') || 0) + 1;
$self->connection->notes('count_relay_attempts', $count);
unless ($count > $self->{_count_relay_max}) {
return $rc, @msg;
};
return Qpsmtpd::DSN->relaying_denied(DENY_DISCONNECT,
"Too many relaying attempts");
}
=head2 Results of other hooks
B<NOTE:> just copied from README.plugins
If we're in a transaction, the results of a callback are stored in
$self->transaction->notes( $code->{name})->{"hook_$hook"}->{return}
If we're in a connection, store things in the connection notes instead.
B<FIXME>: does the above (regarding connection notes) work?
=cut
# vim: ts=2 sw=2 expandtab

231
docs/authentication.md Normal file
View File

@ -0,0 +1,231 @@
# NAME
Authentication framework for qpsmtpd
# DESCRIPTION
Provides support for SMTP AUTH within qpsmtpd transactions, see
[http://www.faqs.org/rfcs/rfc2222.html](http://www.faqs.org/rfcs/rfc2222.html)
[http://www.faqs.org/rfcs/rfc2554.html](http://www.faqs.org/rfcs/rfc2554.html)
for more details.
# USAGE
This code is automatically loaded by Qpsmtpd::SMTP only if a plugin
providing one of the defined ["Auth Hooks"](#auth-hooks) is loaded. The only
time this can happen is if the client process employs the EHLO command to
initiate the SMTP session. If the client uses HELO, the AUTH command is
not available and this module isn't even loaded.
## Plugin Design
An authentication plugin can bind to one or more auth hooks or bind to all
of them at once. See ["Multiple Hook Behavior"](#multiple-hook-behavior) for more details.
All plugins must provide two functions:
- init()
This is the standard function which is called by qpsmtpd for any plugin
listed in config/plugins. Typically, an auth plugin should register at
least one hook, like this:
sub init {
my ($self, $qp) = @_;
$self->register_hook("auth", "authfunction");
}
where in this case "auth" means this plugin expects to support any of
the defined authentication methods.
- authfunction()
The plugin must provide an authentication function which is part of
the register\_hook call. That function will receive the following
six parameters when called:
- $self
A Qpsmtpd::Plugin object, which can be used, for example, to emit log
entries or to send responses to the remote SMTP client.
- $transaction
A Qpsmtpd::Transaction object which can be used to examine information
about the current SMTP session like the remote IP address.
- $mechanism
The lower-case name of the authentication mechanism requested by the
client; either "plain", "login", or "cram-md5".
- $user
Whatever the remote SMTP client sent to identify the user (may be bare
name or fully qualified e-mail address).
- $clearPassword
If the particular authentication method supports unencrypted passwords
(currently PLAIN and LOGIN), which will be the plaintext password sent
by the remote SMTP client.
- $hashPassword
An encrypted form of the remote user's password, using the MD-5 algorithm
(see also the $ticket parameter).
- $ticket
This is the cryptographic challenge which was sent to the client as part
of a CRAM-MD5 transaction. Since the MD-5 algorithm is one-way, the same
$ticket value must be used on the backend to compare with the encrypted
password sent in $hashPassword.
Plugins should perform whatever checking they want and then return one
of the following values (taken from Qpsmtpd::Constants):
- OK
If the authentication has succeeded, the plugin can return this value and
all subsequently registered hooks will be skipped.
- DECLINED
If the authentication has failed, but any additional plugins should be run,
this value will be returned. If none of the registered plugins succeed, the
overall authentication will fail. Normally an auth plugin should return
this value for all cases which do not succeed (so that another auth plugin
can have a chance to authenticate the user).
- DENY
If the authentication has failed, and the plugin wishes this to short circuit
any further testing, it should return this value. For example, a plugin could
register the [auth-plain](https://metacpan.org/pod/auth-plain) hook and immediately fail any connection which is
not trusted (e.g. not in the same network).
Another reason to return DENY over DECLINED would be if the user name matched
an existing account but the password failed to match. This would make a
dictionary-based attack much harder to accomplish. See the included
auth\_vpopmail\_sql plugin for how this might be accomplished.
By returning DENY, no further authentication attempts will be made using the
current method and data. A remote SMTP client is free to attempt a second
auth method if the first one fails.
Plugins may also return an optional message with the return code, e.g.
return (DENY, "If you forgot your password, contact your admin");
and this will be appended to whatever response is sent to the remote SMTP
client. There is no guarantee that the end user will see this information,
though, since some prominent MTA's (produced by M$oft) _helpfully_
hide this information under the default configuration. This message will
be logged locally, if appropriate, based on the configured log level.
# Auth Hooks
The currently defined authentication methods are:
- auth-plain
Any plugin which registers an auth-plain hook will engage in a plaintext
prompted negotiation. This is the least secure authentication method since
both the user name and password are visible in plaintext. Most SMTP clients
will preferentially choose a more secure method if it is advertised by the
server.
- auth-login
A slightly more secure method where the username and password are Base-64
encoded before sending. This is still an insecure method, since it is
trivial to decode the Base-64 data. Again, it will not normally be chosen
by SMTP clients unless a more secure method is not available (or if it fails).
- auth-cram-md5
A cryptographically secure authentication method which employs a one-way
hashing function to transmit the secret information without significant
risk between the client and server. The server provides a challenge key
[$ticket](https://metacpan.org/pod/$ticket), which the client uses to encrypt the user's password.
Then both user name and password are concatenated and Base-64 encoded before
transmission.
This hook must normally have access to the user's plaintext password,
since there is no way to extract that information from the transmitted data.
Since the CRAM-MD5 scheme requires that the server send the challenge
[$ticket](https://metacpan.org/pod/$ticket) before knowing what user is attempting to log in, there is no way
to use any existing MD5-encrypted password (like is frequently used with MySQL).
- auth
A catch-all hook which requires that the plugin support all three preceeding
authentication methods. Any plugins registering the auth hook will be run
only after all other plugins registered for the specific authentication
method which was requested. This allows you to move from more specific
plugins to more general plugins (e.g. local accounts first vs replicated
accounts with expensive network access later).
## Multiple Hook Behavior
If more than one hook is registered for a given authentication method, then
they will be tried in the order that they appear in the config/plugins file
unless one of the plugins returns DENY, which will immediately cease all
authentication attempts for this transaction.
In addition, all plugins that are registered for a specific auth hook will
be tried before any plugins which are registered for the general auth hook.
# VPOPMAIL
There are 4 authentication (smtp-auth) plugins that can be used with
vpopmail.
- auth\_vpopmaild
If you aren't sure which one to use, then use auth\_vpopmaild. It
supports the PLAIN and LOGIN authentication methods,
doesn't require the qpsmtpd process to run with special permissions, and
can authenticate against vpopmail running on another host. It does require
the vpopmaild server to be running.
- auth\_vpopmail
The next best solution is auth\_vpopmail. It requires the p5-vpopmail perl
module and it compiles against libvpopmail.a. There are two catches. The
qpsmtpd daemon must run as the vpopmail user, and you must be running v0.09
or higher for CRAM-MD5 support. The released version is 0.08 but my
CRAM-MD5 patch has been added to the developers repo:
http://github.com/sscanlon/vpopmail
- auth\_vpopmail\_sql
If you are using the MySQL backend for vpopmail, then this module can be
used for smtp-auth. It supports LOGIN, PLAIN, and CRAM-MD5. However, it
does not work with some vpopmail features such as alias domains, service
restrictions, nor does it update vpopmail's last\_auth information.
- auth\_checkpassword
The auth\_checkpassword is a generic authentication module that will work
with any DJB style checkpassword program, including ~vpopmail/bin/vchkpw.
It only supports PLAIN and LOGIN auth methods.
# AUTHOR
John Peacock <jpeacock@cpan.org>
Matt Simerson <msimerson@cpan.org> (added VPOPMAIL)
# COPYRIGHT AND LICENSE
Copyright (c) 2004-2006 John Peacock
Portions based on original code by Ask Bjoern Hansen and Guillaume Filion
This plugin is licensed under the same terms as the qpsmtpd package itself.
Please see the LICENSE file included with qpsmtpd for details.

View File

@ -1,258 +0,0 @@
#
# read this with 'perldoc authentication.pod' ...
#
=head1 NAME
Authentication framework for qpsmtpd
=head1 DESCRIPTION
Provides support for SMTP AUTH within qpsmtpd transactions, see
L<http://www.faqs.org/rfcs/rfc2222.html>
L<http://www.faqs.org/rfcs/rfc2554.html>
for more details.
=head1 USAGE
This code is automatically loaded by Qpsmtpd::SMTP only if a plugin
providing one of the defined L<Auth Hooks> is loaded. The only
time this can happen is if the client process employs the EHLO command to
initiate the SMTP session. If the client uses HELO, the AUTH command is
not available and this module isn't even loaded.
=head2 Plugin Design
An authentication plugin can bind to one or more auth hooks or bind to all
of them at once. See L<Multiple Hook Behavior> for more details.
All plugins must provide two functions:
=over 4
=item * init()
This is the standard function which is called by qpsmtpd for any plugin
listed in config/plugins. Typically, an auth plugin should register at
least one hook, like this:
sub init {
my ($self, $qp) = @_;
$self->register_hook("auth", "authfunction");
}
where in this case "auth" means this plugin expects to support any of
the defined authentication methods.
=item * authfunction()
The plugin must provide an authentication function which is part of
the register_hook call. That function will receive the following
six parameters when called:
=over 4
=item $self
A Qpsmtpd::Plugin object, which can be used, for example, to emit log
entries or to send responses to the remote SMTP client.
=item $transaction
A Qpsmtpd::Transaction object which can be used to examine information
about the current SMTP session like the remote IP address.
=item $mechanism
The lower-case name of the authentication mechanism requested by the
client; either "plain", "login", or "cram-md5".
=item $user
Whatever the remote SMTP client sent to identify the user (may be bare
name or fully qualified e-mail address).
=item $clearPassword
If the particular authentication method supports unencrypted passwords
(currently PLAIN and LOGIN), which will be the plaintext password sent
by the remote SMTP client.
=item $hashPassword
An encrypted form of the remote user's password, using the MD-5 algorithm
(see also the $ticket parameter).
=item $ticket
This is the cryptographic challenge which was sent to the client as part
of a CRAM-MD5 transaction. Since the MD-5 algorithm is one-way, the same
$ticket value must be used on the backend to compare with the encrypted
password sent in $hashPassword.
=back
=back
Plugins should perform whatever checking they want and then return one
of the following values (taken from Qpsmtpd::Constants):
=over 4
=item OK
If the authentication has succeeded, the plugin can return this value and
all subsequently registered hooks will be skipped.
=item DECLINED
If the authentication has failed, but any additional plugins should be run,
this value will be returned. If none of the registered plugins succeed, the
overall authentication will fail. Normally an auth plugin should return
this value for all cases which do not succeed (so that another auth plugin
can have a chance to authenticate the user).
=item DENY
If the authentication has failed, and the plugin wishes this to short circuit
any further testing, it should return this value. For example, a plugin could
register the L<auth-plain> hook and immediately fail any connection which is
not trusted (e.g. not in the same network).
Another reason to return DENY over DECLINED would be if the user name matched
an existing account but the password failed to match. This would make a
dictionary-based attack much harder to accomplish. See the included
auth_vpopmail_sql plugin for how this might be accomplished.
By returning DENY, no further authentication attempts will be made using the
current method and data. A remote SMTP client is free to attempt a second
auth method if the first one fails.
=back
Plugins may also return an optional message with the return code, e.g.
return DENY, "If you forgot your password, contact your admin";
and this will be appended to whatever response is sent to the remote SMTP
client. There is no guarantee that the end user will see this information,
though, since some prominent MTA's (produced by M$oft) I<helpfully>
hide this information under the default configuration. This message will
be logged locally, if appropriate, based on the configured log level.
=head1 Auth Hooks
The currently defined authentication methods are:
=over 4
=item * auth-plain
Any plugin which registers an auth-plain hook will engage in a plaintext
prompted negotiation. This is the least secure authentication method since
both the user name and password are visible in plaintext. Most SMTP clients
will preferentially choose a more secure method if it is advertised by the
server.
=item * auth-login
A slightly more secure method where the username and password are Base-64
encoded before sending. This is still an insecure method, since it is
trivial to decode the Base-64 data. Again, it will not normally be chosen
by SMTP clients unless a more secure method is not available (or if it fails).
=item * auth-cram-md5
A cryptographically secure authentication method which employs a one-way
hashing function to transmit the secret information without significant
risk between the client and server. The server provides a challenge key
L<$ticket>, which the client uses to encrypt the user's password.
Then both user name and password are concatenated and Base-64 encoded before
transmission.
This hook must normally have access to the user's plaintext password,
since there is no way to extract that information from the transmitted data.
Since the CRAM-MD5 scheme requires that the server send the challenge
L<$ticket> before knowing what user is attempting to log in, there is no way
to use any existing MD5-encrypted password (like is frequently used with MySQL).
=item * auth
A catch-all hook which requires that the plugin support all three preceeding
authentication methods. Any plugins registering the auth hook will be run
only after all other plugins registered for the specific authentication
method which was requested. This allows you to move from more specific
plugins to more general plugins (e.g. local accounts first vs replicated
accounts with expensive network access later).
=back
=head2 Multiple Hook Behavior
If more than one hook is registered for a given authentication method, then
they will be tried in the order that they appear in the config/plugins file
unless one of the plugins returns DENY, which will immediately cease all
authentication attempts for this transaction.
In addition, all plugins that are registered for a specific auth hook will
be tried before any plugins which are registered for the general auth hook.
=head1 VPOPMAIL
There are 4 authentication (smtp-auth) plugins that can be used with
vpopmail.
=over 4
=item auth_vpopmaild
If you aren't sure which one to use, then use auth_vpopmaild. It
supports the PLAIN and LOGIN authentication methods,
doesn't require the qpsmtpd process to run with special permissions, and
can authenticate against vpopmail running on another host. It does require
the vpopmaild server to be running.
=item auth_vpopmail
The next best solution is auth_vpopmail. It requires the p5-vpopmail perl
module and it compiles against libvpopmail.a. There are two catches. The
qpsmtpd daemon must run as the vpopmail user, and you must be running v0.09
or higher for CRAM-MD5 support. The released version is 0.08 but my
CRAM-MD5 patch has been added to the developers repo:
http://github.com/sscanlon/vpopmail
=item auth_vpopmail_sql
If you are using the MySQL backend for vpopmail, then this module can be
used for smtp-auth. It supports LOGIN, PLAIN, and CRAM-MD5. However, it
does not work with some vpopmail features such as alias domains, service
restrictions, nor does it update vpopmail's last_auth information.
=item auth_checkpassword
The auth_checkpassword is a generic authentication module that will work
with any DJB style checkpassword program, including ~vpopmail/bin/vchkpw.
It only supports PLAIN and LOGIN auth methods.
=back
=head1 AUTHOR
John Peacock <jpeacock@cpan.org>
Matt Simerson <msimerson@cpan.org> (added VPOPMAIL)
=head1 COPYRIGHT AND LICENSE
Copyright (c) 2004-2006 John Peacock
Portions based on original code by Ask Bjoern Hansen and Guillaume Filion
This plugin is licensed under the same terms as the qpsmtpd package itself.
Please see the LICENSE file included with qpsmtpd for details.
=cut

183
docs/config.md Normal file
View File

@ -0,0 +1,183 @@
# Qpsmtpd configuration
The default way of setting config values is placing files with the
name of the config variable in the config directory `config/`, like
qmail's `/var/qmail/control/` directory. NB: `/var/qmail/control` (or
`$ENV{QMAIL}/control`) is used if a file does not exist in `config/`.
The location of the `config/` directory can be set via the
`QPSMTPD_CONFIG` environment variable and defaults to the current
working directory.
Any empty line or lines starting with `#` are ignored. You may use a
plugin which hooks the `config` hook to store the settings in some other
way. See ["plugins.pod" in docs](https://metacpan.org/pod/docs#plugins.pod) and ["hooks.pod" in docs](https://metacpan.org/pod/docs#hooks.pod) for more info on this.
Some settings still have to go in files, because they are loaded before
any plugin can return something via the `config` hook: `me`, `logging`,
`plugin_dirs` and of course `plugins`.
## Core settings
These settings are used by the qpsmtpd core. Any other setting is (hopefully)
documented by the corresponding plugin. Some settings of important plugins
are shown below in ["Plugin settings"](#plugin-settings).
- plugins
The main config file, where all used plugins and their arguments are listed.
- me
Sets the hostname which is used all over the place: in the greeting message,
the _Received: _header, ...
Default is whatever Sys::Hostname's hostname() returns.
- plugin\_dirs
Where to search for plugins (one directory per line), defaults to `./plugins`.
- logging
Sets the primary logging destination, see `plugins/logging/*`. Format
is the same as it's used for the `plugins` config file. __NOTE:__ only
the first non empty line is used (lines starting with `#` are counted
as empty).
- loglevel
This is not used anymore, _only_ if no `logging/` plugin is in use. Use a
logging plugin.
- databytes
Maximum size a message may be. Without this setting, there is no limit on the
size. Should be something less than the backend MTA has set as it's maximum
message size (if there is one).
- size\_threshold
When a message is greater than the size given in this config file, it will be
spooled to disk. You probably want to enable spooling to disk for most virus
scanner plugins and `spamassassin`.
- smtpgreeting
Override the default SMTP greeting with this string.
- spool\_dir
Where temporary files are stored, defaults to `~/tmp/`.
- spool\_perms
Permissions of the _spool\_dir_, default is `0700`. You probably have to
change the defaults for some scanners (e.g. the `clamdscan` plugin).
- timeout
- timeoutsmtpd
Set the timeout for the clients, `timeoutsmtpd` is the qmail smtpd control
file, `timeout` the qpsmtpd file. Default is 1200 seconds.
- tls\_before\_auth
If set to a true value, clients will have to initiate an SSL secured
connection before any auth succeeds, defaults to `0`.
## Plugin settings files
- rcpthosts, morercpthosts
Plugin: `rcpt_ok`
Domains listed in these files will be accepted as valid local domains,
anything else is rejected with a `Relaying denied` message. If an entry
in the `rcpthosts` file starts with a `.`, mails to anything ending with
this string will be accepted, e.g.:
example.com
.example.com
will accept mails for `user@example.com` and `user@something.example.com`.
The `morercpthosts` file is just checked for exact (case insensitive)
matches.
- `hosts_allow`
Plugin: `hosts_allow`.
Don't use this config file. The plugin itself is required to set the
maximum number of concurrent connections. This config setting should
only be used for some extremly rude clients: if list is too big it will
slow down accepting new connections.
- relayclients
- morerelayclients
Plugin: `check_relay`
Allow relaying for hosts listed in this file. The `relayclients` file accepts
IPs and CIDR entries. The `morercpthosts` file accepts IPs and `prefixes`
like `192.168.2.` (note the trailing dot!). With the given example any host
which IP starts with `192.168.2.` may relay via us.
- `dnsbl_zones`
Plugin: `dnsbl`
This file specifies the RBL zones list, used by the dnsbl plugin. Ihe IP
address of each connecting host will be checked against each zone given.
A few sample DNSBLs are listed in the sample config file, but you should
evaluate the efficacy and listing policies of a DNSBL before using it.
See also `dnsbl_allow` and `dnsbl_rejectmsg` in the documentation of the
`dnsbl` plugin
- `resolvable_fromhost`
Plugin: `resolvable_fromhost`
Reject sender addresses where the MX is unresolvable, i.e. a boolean value
is the only value in this file. If the MX resolves to something, reject the
sender address if it resolves to something listed in the
`invalid_resolvable_fromhost` config file. The _invalid\_resolvable\_fromhost_
expects IP addresses or CIDR (i.e. `network/mask` values) one per line, IPv4
only currenlty.
## Plugin settings arguments
These are arguments that can be set on the config/plugins line, after the name
of the plugin. These config options are available to all plugins.
- loglevel
Adjust the quantity of logging for the plugin. See docs/logging.pod
- reject
plugin reject [ 0 | 1 | naughty ]
Should the plugin reject mail?
The special 'naughty' case will mark the connection as a naughty. Most plugins
skip processing naughty connections. Filtering plugins can learn from them.
Naughty connections are terminated up by the __naughty__ plugin.
Plugins that use $self->get\_reject() or $self->get\_reject\_type() will
automatically honor this setting.
- `reject_type`
plugin reject_type [ perm | temp | disconnect | temp_disconnect ]
Default: perm
Values with temp in the name return a 4xx code and the others return a 5xx
code.
The `reject_type` argument and the corresponding `get_reject_type()` method
provides a standard way for plugins to automatically return the selected
rejection type, as chosen by the config setting, the plugin author, or the
`get_reject_type()` method.
Plugins that are updated to use the `$self->get_reject()` or
`$self->get_reject_type()` methods will automatically honor this setting.

View File

@ -1,200 +0,0 @@
=head1 Qpsmtpd configuration
The default way of setting config values is placing files with the
name of the config variable in the config directory F<config/>, like
qmail's F</var/qmail/control/> directory. NB: F</var/qmail/control> (or
F<$ENV{QMAIL}/control>) is used if a file does not exist in C<config/>.
The location of the C<config/> directory can be set via the
I<QPSMTPD_CONFIG> environment variable and defaults to the current
working directory.
Any empty line or lines starting with C<#> are ignored. You may use a
plugin which hooks the C<config> hook to store the settings in some other
way. See L<docs/plugins.pod> and L<docs/hooks.pod> for more info on this.
Some settings still have to go in files, because they are loaded before
any plugin can return something via the C<config> hook: C<me>, C<logging>,
C<plugin_dirs> and of course C<plugins>. B<FIXME: more?>
=head2 Core settings
These settings are used by the qpsmtpd core. Any other setting is (hopefully)
documented by the corresponding plugin. Some settings of important plugins
are shown below in L</Plugin settings>.
=over 4
=item plugins
The main config file, where all used plugins and their arguments are listed.
=item me
Sets the hostname which is used all over the place: in the greeting message,
the I<Received: >header, ...
Default is whatever Sys::Hostname's hostname() returns.
=item plugin_dirs
Where to search for plugins (one directory per line), defaults to F<./plugins>.
=item logging
Sets the primary logging destination, see F<plugins/logging/*>. Format
is the same as it's used for the F<plugins> config file. B<NOTE:> only
the first non empty line is used (lines starting with C<#> are counted
as empty).
=item loglevel
This is not used anymore, I<only> if no F<logging/> plugin is in use. Use a
logging plugin.
=item databytes
Maximum size a message may be. Without this setting, there is no limit on the
size. Should be something less than the backend MTA has set as it's maximum
message size (if there is one).
=item size_threshold
When a message is greater than the size given in this config file, it will be
spooled to disk. You probably want to enable spooling to disk for most virus
scanner plugins and F<spamassassin>.
=item smtpgreeting
Override the default SMTP greeting with this string.
=item spool_dir
Where temporary files are stored, defaults to F<~/tmp/>.
=item spool_perms
Permissions of the I<spool_dir>, default is C<0700>. You probably have to
change the defaults for some scanners (e.g. the F<clamdscan> plugin).
=item timeout
=item timeoutsmtpd
Set the timeout for the clients, C<timeoutsmtpd> is the qmail smtpd control
file, C<timeout> the qpsmtpd file. Default is 1200 seconds.
=item tls_before_auth
If set to a true value, clients will have to initiate an SSL secured
connection before any auth succeeds, defaults to C<0>.
=back
=head2 Plugin settings files
=over 4
=item rcpthosts, morercpthosts
Plugin: I<rcpt_ok>
Domains listed in these files will be accepted as valid local domains,
anything else is rejected with a C<Relaying denied> message. If an entry
in the C<rcpthosts> file starts with a C<.>, mails to anything ending with
this string will be accepted, e.g.:
example.com
.example.com
will accept mails for C<user@example.com> and C<user@something.example.com>.
The C<morercpthosts> file is just checked for exact (case insensitive)
matches.
=item hosts_allow
Plugin: F<hosts_allow>.
Don't use this config file. The plugin itself is required to set the
maximum number of concurrent connections. This config setting should
only be used for some extremly rude clients: if list is too big it will
slow down accepting new connections.
=item relayclients
=item morerelayclients
Plugin: F<check_relay>
Allow relaying for hosts listed in this file. The C<relayclients> file accepts
IPs and CIDR entries. The C<morercpthosts> file accepts IPs and C<prefixes>
like C<192.168.2.> (note the trailing dot!). With the given example any host
which IP starts with C<192.168.2.> may relay via us.
=item dnsbl_zones
Plugin: F<dnsbl>
This file specifies the RBL zones list, used by the dnsbl plugin. Ihe IP
address of each connecting host will be checked against each zone given.
A few sample DNSBLs are listed in the sample config file, but you should
evaluate the efficacy and listing policies of a DNSBL before using it.
See also C<dnsbl_allow> and C<dnsbl_rejectmsg> in the documentation of the
C<dnsbl> plugin
=item resolvable_fromhost
Plugin: F<resolvable_fromhost>
Reject sender addresses where the MX is unresolvable, i.e. a boolean value
is the only value in this file. If the MX resolves to something, reject the
sender address if it resolves to something listed in the
F<invalid_resolvable_fromhost> config file. The I<invalid_resolvable_fromhost>
expects IP addresses or CIDR (i.e. C<network/mask> values) one per line, IPv4
only currenlty.
=back
=head2 Plugin settings arguments
These are arguments that can be set on the config/plugins line, after the name
of the plugin. These config options are available to all plugins.
=over 4
=item loglevel
Adjust the quantity of logging for the plugin. See docs/logging.pod
=item reject
plugin reject [ 0 | 1 | naughty ]
Should the plugin reject mail?
The special 'naughty' case will mark the connection as a naughty. Most plugins
skip processing naughty connections. Filtering plugins can learn from them.
Naughty connections are terminated up by the B<naughty> plugin.
Plugins that use $self->get_reject() or $self->get_reject_type() will
automatically honor this setting.
=item reject_type
plugin reject_type [ perm | temp | disconnect | temp_disconnect ]
Default: perm
Values with temp in the name return a 4xx code and the others return a 5xx
code.
The I<reject_type> argument and the corresponding get_reject_type() method
provides a standard way for plugins to automatically return the selected
rejection type, as chosen by the config setting, the plugin author, or the
get_reject_type() method.
Plugins that are updated to use the $self->get_reject() or
$self->get_reject_type() methods will automatically honor this setting.
=back
=cut

View File

@ -1,36 +1,35 @@
# Developing Qpsmtpd
=head1 Developing Qpsmtpd ## Mailing List
=head2 Mailing List
All qpsmtpd development happens on the qpsmtpd mailing list. All qpsmtpd development happens on the qpsmtpd mailing list.
Subscribe by sending mail to qpsmtpd-subscribe@perl.org Subscribe by sending mail to qpsmtpd-subscribe@perl.org
=head2 Git ## Git
We use git for version control. We use git for version control.
Ask owns the master repository at git://github.com/smtpd/qpsmtpd.git The master repository is at git://github.com/smtpd/qpsmtpd.git
We suggest using github to host your repository -- it makes your We suggest using github to host your repository -- it makes your
changes easily accessible for pulling into the master. After you changes easily accessible for pulling into the master. After you
create a github account, go to create a github account, go to
http://github.com/smtpd/qpsmtpd/tree/master and click on the "fork" [the master repository](http://github.com/smtpd/qpsmtpd/tree/master) and click on the "fork"
button to get your own repository. button to get your own repository.
=head3 Making a working Copy ### Making a working Copy
git clone git@github.com:username/qpsmtpd.git qpsmtpd git clone git@github.com:username/qpsmtpd.git qpsmtpd
will check out your copy into a directory called qpsmtpd will check out your copy into a directory called qpsmtpd
=head3 Making a branch for your change ### Making a branch for your change
As a general rule, you'll be better off if you do your changes on a As a general rule, you'll be better off if you do your changes on a
branch - preferably a branch per unrelated change. branch - preferably a branch per unrelated change.
You can use the C<git branch> command to see which branch you are on. You can use the `git branch` command to see which branch you are on.
The easiest way to make a new branch is The easiest way to make a new branch is
@ -39,7 +38,7 @@ The easiest way to make a new branch is
This will create a new branch with the name "topic/my-great-change" This will create a new branch with the name "topic/my-great-change"
(and your current commit as the starting point). (and your current commit as the starting point).
=head3 Committing a change ### Committing a change
Edit the appropriate files, and be sure to run the test suite. Edit the appropriate files, and be sure to run the test suite.
@ -56,32 +55,32 @@ When you're ready to check it in...
git log -p # review your commit a last time git log -p # review your commit a last time
git push origin # to send to github git push origin # to send to github
=head3 Commit Descriptions ### Commit Descriptions
Though not required, it's a good idea to begin the commit message with Though not required, it's a good idea to begin the commit message with
a single short (less than 50 character) line summarizing the change, a single short (less than 50 character) line summarizing the change,
followed by a blank line and then a more thorough description. Tools followed by a blank line and then a more thorough description. Tools
that turn commits into email, for example, use the first line on the that turn commits into email, for example, use the first line on the
Subject: line and the rest of the commit in the body. Subject: line and the rest of the commit in the body.
(From: L<git-commit(1)>) (From: [git-commit(1)](http://man.he.net/man1/git-commit))
=head3 Submit patches by mail ### Submit patches by mail
The best way to submit patches to the project is to send them to the The best way to submit patches to the project is to send them to the
mailing list for review. Use the C<git format-patch> command to mailing list for review. Use the `git format-patch` command to
generate patches ready to be mailed. For example: generate patches ready to be mailed. For example:
git format-patch HEAD~3 git format-patch HEAD~3
will put each of the last three changes in files ready to be mailed will put each of the last three changes in files ready to be mailed
with the C<git send-email> tool (it might be a good idea to send them with the `git send-email` tool (it might be a good idea to send them
to yourself first as a test). to yourself first as a test).
Sending patches to the mailing list is the most effective way to Sending patches to the mailing list is the most effective way to
submit changes, although it helps if you at the same time also commit submit changes, although it helps if you at the same time also commit
them to a git repository (for example on github). them to a git repository (for example on github).
=head3 Merging changes back in from the master repository ### Merging changes back in from the master repository
Tell git about the master repository. We're going to call it 'smtpd' Tell git about the master repository. We're going to call it 'smtpd'
for now, but you could call it anything you want. You only have to do for now, but you could call it anything you want. You only have to do
@ -116,7 +115,7 @@ you might get yourself into an odd situation.
Conflicts happen because upstream committers may make minor tweaks to Conflicts happen because upstream committers may make minor tweaks to
your change before applying it. your change before applying it.
=head3 Throwing away changes ### Throwing away changes
If you get your working copy into a state you don't like, you can If you get your working copy into a state you don't like, you can
always revert to the last commit: always revert to the last commit:
@ -131,13 +130,12 @@ If you make a mistake with this, git is pretty good about keeping your
commits around even as you merge, rebase and reset away. This log of commits around even as you merge, rebase and reset away. This log of
your git changes is called with "git reflog". your git changes is called with "git reflog".
=head3 Applying other peoples changes ### Applying other peoples changes
If you get a change in an email with the patch, one easy way to apply If you get a change in an email with the patch, one easy way to apply
other peoples changes is to use C<git am>. That will go ahead and other peoples changes is to use `git am`. That will go ahead and
commit the change. To modify it, you can use C<git commit --amend>. commit the change. To modify it, you can use `git commit --amend`.
If the changes are in a repository, you can add that repository with If the changes are in a repository, you can add that repository with
"git remote add" and then either merge them in with "git merge" or "git remote add" and then either merge them in with "git merge" or
pick just the relevant commits with "git cherry-pick". pick just the relevant commits with "git cherry-pick".

778
docs/hooks.md Normal file
View File

@ -0,0 +1,778 @@
# 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 `Qpsmtpd::Plugin` object of the currently
running plugin as the first argument. A `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.
__NOTE__: for some hooks (post-fork, post-connection, disconnect, deny, ok) the
return values are ignored. This does __not__ mean you can return anything you
want. It just means the return value is discarded and you can not disconnect
a client with `DENY_DISCONNECT`. The rule to return `DECLINED` to run the
next plugin for this hook (or return `OK` / `DONE` to stop processing)
still applies.
## 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 is available in `qpsmtpd-forkserver` and `qpsmtpd-prefork` flavors.
__NOTE:__ You should not use this hook to do major work and / or use lookup
methods which (_may_) take some time, like DNS lookups. This will slow down
__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],
# );
__NOTE:__ the `$transaction` is of course `undef` at this time.
Allowed return codes are
- DENY / DENY\_DISCONNECT
returns a __550__ to the client and ends the connection
- DENYSOFT / DENYSOFT\_DISCONNECT
returns a __451__ to the client and ends the connection
Anything else is ignored.
Example plugins are `hosts_allow` and `connection_time`.
## 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;
__NOTE:__ in fact you get passed two more arguments, which are `undef` at this
early stage of the connection, so ignore them.
Allowed return codes are
- OK
Stop processing plugins, give the default response
- DECLINED
Process the next plugin
- DONE
Stop processing plugins and dont give the default response, i.e. the plugin
gave the response
- DENY
Return hard failure code and disconnect
- DENYSOFT
Return soft failure code and disconnect
Example plugin for this hook is the `check_relay` plugin.
## hook\_helo / hook\_ehlo
It is called after the client sent __EHLO__ (hook\_ehlo) or __HELO__ (hook\_helo)
Allowed return codes are
- DENY
Return a 550 code
- DENYSOFT
Return a __450__ code
- DENY\_DISCONNECT / DENYSOFT\_DISCONNECT
as above but with disconnect
- DONE
Qpsmtpd wont do anything, the plugin sent the message
- DECLINED
Qpsmtpd will send the standard __EHLO__/__HELO__ answer, of course only
if all plugins hooking _helo/ehlo_ return _DECLINED_.
Arguments of this hook are
my ($self, $transaction, $host) = @_;
# $host: the name the client sent in the
# (EH|HE)LO line
__NOTE:__ `$transaction` is `undef` at this point.
## hook\_mail\_pre
After the `MAIL FROM:` line sent by the client is broken into
pieces by the `hook_mail_parse()`, this hook recieves the results.
This hook may be used to pre-accept adresses without the surrounding
`<>` (by adding them) or addresses like `<user@example.com.>` or `<user@example.com >` by
removing the trailing "." / " ".
Expected return values are `OK` and an address which must be parseable
by `Qpsmtpd::Address->parse()` on success or any other constant to
indicate failure.
Arguments are
my ($self, $transaction, $addr) = @_;
## hook\_mail
Called right after the envelope sender line is parsed (the `MAIL FROM:`
command). The plugin gets passed a `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).
__NOTE:__ According to the SMTP protocol, you can not reject an invalid
sender until after the __RCPT__ stage (except for protocol errors, i.e.
syntax errors in address). So store it in an `$transaction->note()` and
process it later in an rcpt hook.
Allowed return codes are
- OK
sender allowed
- DENY
Return a hard failure code
- DENYSOFT
Return a soft failure code
- DENY\_DISCONNECT / DENYSOFT\_DISCONNECT
as above but with disconnect
- DECLINED
next plugin (if any)
- DONE
skip further processing, plugin sent response
Arguments for this hook are
my ($self,$transaction, $sender, %args) = @_;
# $sender: an Qpsmtpd::Address object for
# sender of the message
Example plugins for the `hook_mail` are `resolvable_fromhost`
and `badmailfrom`.
## hook\_rcpt\_pre
See `hook_mail_pre`, s/MAIL FROM:/RCPT TO:/.
## hook\_rcpt
This hook is called after the client sent an `RCPT TO:` command (after
parsing the line). The given argument is parsed by *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
- OK
recipient allowed
- DENY
Return a hard failure code, for example for an _User does not exist here_
message.
- DENYSOFT
Return a soft failure code, for example if the connect to a user lookup
database failed
- DENY\_DISCONNECT / DENYSOFT\_DISCONNECT
as above but with disconnect
- DONE
skip further processing, plugin sent response
Arguments are
my ($self, $transaction, $recipient, %args) = @_;
# $rcpt = Qpsmtpd::Address object with
# the given recipient address
Example plugin is `rcpt_ok`.
## hook\_data
After the client sent the __DATA__ command, before any data of the message
was sent, this hook is called.
__NOTE:__ This hook, like __EHLO__, __VRFY__, __QUIT__, __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 `earlytalker`
plugin may be configured to check at this hook for \`\`early talkers''.
Allowed return codes are
- DENY
Return a hard failure code
- DENYSOFT
Return a soft failure code
- DENY\_DISCONNECT / DENYSOFT\_DISCONNECT
as above but with disconnect
- DONE
Plugin took care of receiving data and calling the queue (not recommended)
__NOTE:__ The only real use for _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''.
Arguments:
my ($self, $transaction) = @_;
Example plugin is `greylisting`.
## hook\_received\_line
If you wish to provide your own Received header line, do it here. You can use
or discard any of the given arguments (see below).
Allowed return codes:
- OK, $string
use this string for the Received header.
- anything else
use the default Received header
Arguments are
my ($self, $transaction, $smtp, $auth, $sslinfo) = @_;
# $smtp - the SMTP type used (e.g. "SMTP" or "ESMTP").
# $auth - the Auth header additionals.
# $sslinfo - information about SSL for the header.
## data\_headers\_end
This hook fires after all header lines of the message data has been received.
Defaults to doing nothing, just continue processing. At this step,
the sender is not waiting for a reply, but we can try and prevent him from
sending the entire message by disconnecting immediately. (Although it is
likely the packets are already in flight due to buffering and pipelining).
__NOTE:__ BE CAREFUL! If you drop the connection legal MTAs will retry again
and again, spammers will probably not. This is not RFC compliant and can lead
to an unpredictable mess. Use with caution.
Why this hook may be useful for you, see
[http://www.nntp.perl.org/group/perl.qpsmtpd/2009/02/msg8502.html](http://www.nntp.perl.org/group/perl.qpsmtpd/2009/02/msg8502.html), ff.
Allowed return codes:
- DENY\_DISCONNECT
Return __554 Message denied__ and disconnect
- DENYSOFT\_DISCONNECT
Return __421 Message denied temporarily__ and disconnect
- DECLINED
Do nothing
Arguments:
my ($self, $transaction) = @_;
__FIXME:__ check arguments
## hook\_data\_post
The `data_post` hook is called after the client sent the final `.\r\n`
of a message, before the mail is sent to the queue.
Allowed return codes are
- DENY
Return a hard failure code
- DENYSOFT
Return a soft failure code
- DENY\_DISCONNECT / DENYSOFT\_DISCONNECT
as above but with disconnect
- DONE
skip further processing (message will not be queued), plugin gave the response.
__NOTE:__ just returning _OK_ from a special queue plugin does (nearly)
the same (i.e. dropping the mail to `/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!
Arguments:
my ($self, $transaction) = @_;
Example plugins: `spamassassin`, `virus/clamdscan`
## 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
- DONE
no queuing is done
- OK / DECLINED
queue the mail
## hook\_queue
When all `data_post` hooks accepted the message, this hook is called. It
is used to queue the message to the \`\`backend''.
Allowed return codes:
- DONE
skip further processing (plugin gave response code)
- OK
Return success message, i.e. tell the client the message was queued (this
may be used to drop the message silently).
- DENY
Return hard failure code
- DENYSOFT
Return soft failure code, i.e. if disk full or other temporary queuing
problems
Arguments:
my ($self, $transaction) = @_;
Example plugins: all `queue/*` plugins
## hook\_queue\_post
This hook is called always after `hook_queue`. If the return code is
__not__ _OK_, a message (all remaining return values) with level _LOGERROR_
is written to the log.
Arguments are
my $self = shift;
__NOTE:__ `$transaction` is not valid at this point, therefore not mentioned.
## hook\_reset\_transaction
This hook will be called several times. At the beginning of a transaction
(i.e. when the client sends a __MAIL FROM:__ command the first time),
after queueing the mail and every time a client sends a __RSET__ command.
Arguments are
my ($self, $transaction) = @_;
__NOTE:__ don't rely on `$transaction` being valid at this point.
## hook\_quit
After the client sent a __QUIT__ command, this hook is called (before the
`hook_disconnect`).
Allowed return codes
- DONE
plugin sent response
- DECLINED
next plugin and / or qpsmtpd sends response
Arguments: the only argument is `$self`
Expample plugin is the `quit_fortune` plugin.
## hook\_disconnect
This hook will be called from several places: After a plugin returned
`DENY(|SOFT)_DISCONNECT`, before connection is disconnected or after the
client sent the `QUIT` command, AFTER the quit hook and ONLY if no plugin
hooking `hook_quit` returned `DONE`.
All return values are ignored, arguments are just `$self`
Example plugin is `logging/file`
## hook\_post\_connection
This is the counter part of the `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 __QUIT__). This hook is not called if the qpsmtpd
instance is killed.
The only argument is `$self` and all return codes are ignored, it would
be too late anyway :-).
Example: `connection_time`
# Parsing Hooks
Before the line from the client is parsed by
`Qpsmtpd::Command->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:
- $self
the plugin object
- $cmd
the command (i.e. the first word of the line) sent by the client
- $line
the line sent by the client without the first word
Expected return values from this sub are _DENY_ and a reason which is
sent to the client or _OK_ and the `$line` broken into pieces according
to the syntax rules for the command.
__NOTE: ignore the example from `Qpsmtpd::Command`, the `unrecognized_command_parse` hook was never implemented,...__
## `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 __HELO__ / __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
_HELO_ hosts with FQDNs and you have a legal client which can not be
changed to send his FQDN, this is the right place.
## hook\_mail\_parse / hook\_rcpt\_parse
The provided sub routine must return two or more values. The first is
either _OK_ to indicate that parsing of the line was successfull
or anything else to bail out with _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 _<_ and
_>_, don't add them here, use the `hook_mail_pre()` /
`hook_rcpt_pre()` methods for this). All other arguments are
sent to the `mail / rcpt` hook as __MAIL__ / __RCPT__ parameters (see
RFC 1869 _SMTP Service Extensions_ for more info). Note that
the mail and rcpt hooks expect a list of key/value pairs as the
last arguments.
## hook\_auth\_parse
__FIXME...__
# 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.
## hook\_logging
This hook is called when a log message is written, for example in a plugin
it fires if someone calls `$self->log($level, $msg);`. Allowed
return codes are
- DECLINED
next logging plugin
- OK
(not _DONE_, as some might expect!) ok, plugin logged the message
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
__NOTE:__ `$transaction` may be `undef`, depending when / where this hook
is called. It's probably best not to try acessing it.
All `logging/*` plugins can be used as example plugins.
## hook\_deny
This hook is called after a plugin returned _DENY_, _DENYSOFT_,
_DENY\_DISCONNECT_ or _DENYSOFT\_DISCONNECT_. All return codes are ignored,
arguments are
my ($self, $transaction, $prev_plugin, $return, $return_text) = @_;
__NOTE:__ `$transaction` may be `undef`, depending when / where this hook
is called. It's probably best not to try acessing it.
Example plugin for this hook is `logging/adaptive`.
## hook\_ok
The counter part of `hook_deny`, it is called after a plugin __did not__
return _DENY_, _DENYSOFT_, _DENY\_DISCONNECT_ or _DENYSOFT\_DISCONNECT_.
All return codes are ignored, arguments are
my ( $self, $transaction, $prev_plugin, $return, $return_text ) = @_;
__NOTE:__ `$transaction` may be `undef`, depending when / where this hook
is called. It's probably best not to try acessing it.
## hook\_config
Called when a config file is requested, for example in a plugin it fires
if someone calls `my @cfg = $self->qp->config($cfg_name);`.
Allowed return codes are
- DECLINED
plugin didn't find the requested value
- OK, @values
requested values as `@list`, example:
return (OK, @{$config{$key}})
if exists $config{$key};
return (DECLINED);
Arguments:
my ($self,$transaction,@keys) = @_;
# @keys: the requested config item(s)
__NOTE:__ `$transaction` may be `undef`, depending when / where this hook
is called. It's probably best not to try acessing it.
Example plugin is `http_config` from the qpsmtpd distribution.
## hook\_user\_config
Called when a per-user configuration directive is requested, for example
if someone calls `my @cfg = $rcpt->config($cfg_name);`.
Allowed return codes are
- DECLINED
plugin didn't find the requested value
- OK, @values
requested values as `@list`, example:
return (OK, @{$config{$key}})
if exists $config{$key};
return (DECLINED);
Arguments:
my ($self,$transaction,$user,@keys) = @_;
# @keys: the requested config item(s)
Example plugin is `user_config` from the qpsmtpd distribution.
## 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:
- DENY\_DISCONNECT
Return __521__ and disconnect the client
- DENY
Return __500__
- DONE
Qpsmtpd wont do anything; the plugin responded, this is what you want to
return, if you are implementing new commands
- Anything else...
Return __500 Unrecognized command__
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
__NOTE:__ `$transaction` may be `undef`, depending when / where this hook
is called. It's probably best not to try acessing it.
Example plugin is `tls`.
## hook\_help
This hook triggers if a client sends the __HELP__ command, allowed return
codes are:
- DONE
Plugin gave the answer.
- DENY
The client will get a `syntax error` message, probably not what you want,
better use
$self->qp->respond(502, "Not implemented.");
return DONE;
Anything else will be send as help answer.
Arguments are
my ($self, $transaction, @args) = @\_;
with `@args` being the arguments from the client's command.
## hook\_vrfy
If the client sents the __VRFY__ command, this hook is called. Default is to
return a message telling the user to just try sending the message.
Allowed return codes:
- OK
Recipient Exists
- DENY
Return a hard failure code
- DONE
Return nothing and move on
- Anything Else...
Return a __252__
Arguments are:
my ($self) = shift;
## hook\_noop
If the client sents the __NOOP__ command, this hook is called. Default is to
return `250 OK`.
Allowed return codes are:
- DONE
Plugin gave the answer
- DENY\_DISCONNECT
Return error code and disconnect client
- DENY
Return error code.
- Anything Else...
Give the default answer of __250 OK__.
Arguments are
my ($self,$transaction,@args) = @_;
# Authentication hooks
See `docs/authentication.pod`.

View File

@ -1,928 +0,0 @@
#
# This file is best read with ``perldoc plugins.pod''
#
###
# Conventions:
# plugin names: F<myplugin>
# 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 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.
B<NOTE>: for some hooks (post-fork, post-connection, disconnect, deny, ok) the
return values are ignored. This does B<not> mean you can return anything you
want. It just means the return value is discarded and you can not disconnect
a client with I<DENY_DISCONNECT>. The rule to return I<DECLINED> to run the
next plugin for this hook (or return I<OK> / I<DONE> to stop processing)
still applies.
=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 is available in F<qpsmtpd-forkserver> and F<qpsmtpd-prefork> flavors.
=cut
NOT FOR: 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>user@example.com.E<gt>> or I<E<lt>user@example.com 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<resolvable_fromhost>
and F<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<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_received_line
If you wish to provide your own Received header line, do it here. You can use
or discard any of the given arguments (see below).
Allowed return codes:
=over 4
=item OK, $string
use this string for the Received header.
=item anything else
use the default Received header
=back
Arguments are
my ($self, $transaction, $smtp, $auth, $sslinfo) = @_;
# $smtp - the SMTP type used (e.g. "SMTP" or "ESMTP").
# $auth - the Auth header additionals.
# $sslinfo - information about SSL for the header.
=head2 data_headers_end
This hook fires after all header lines of the message data has been received.
Defaults to doing nothing, just continue processing. At this step,
the sender is not waiting for a reply, but we can try and prevent him from
sending the entire message by disconnecting immediately. (Although it is
likely the packets are already in flight due to buffering and pipelining).
B<NOTE:> BE CAREFUL! If you drop the connection legal MTAs will retry again
and again, spammers will probably not. This is not RFC compliant and can lead
to an unpredictable mess. Use with caution.
Why this hook may be useful for you, see
L<http://www.nntp.perl.org/group/perl.qpsmtpd/2009/02/msg8502.html>, ff.
Allowed return codes:
=over 4
=item DENY_DISCONNECT
Return B<554 Message denied> and disconnect
=item DENYSOFT_DISCONNECT
Return B<421 Message denied temporarily> and disconnect
=item DECLINED
Do nothing
=back
Arguments:
my ($self, $transaction) = @_;
B<FIXME:> check arguments
=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
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_plugin, $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_plugin, $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, @values
requested values as C<@list>, example:
if (exists $config{$key}) {
return OK, @{$config{$key}}
};
return DECLINED;
=back
Arguments:
my ($self,$transaction,@keys) = @_;
# @keys: 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_user_config
Called when a per-user configuration directive is requested, for example
if someone calls C<my @cfg = $rcpt-E<gt>config($cfg_name);>.
Allowed return codes are
=over 4
=item DECLINED
plugin didn't find the requested value
=item OK, @values
requested values as C<@list>, example:
if (exists $config{$key}) {
return OK, @{$config{$key}}
};
return DECLINED;
=back
Arguments:
my ($self,$transaction,$user,@keys) = @_;
# @keys: the requested config item(s)
Example plugin is F<user_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_help
This hook triggers if a client sends the B<HELP> command, allowed return
codes are:
=over 4
=item DONE
Plugin gave the answer.
=item DENY
The client will get a C<syntax error> message, probably not what you want,
better use
$self->qp->respond(502, "Not implemented.");
return DONE;
=back
Anything else will be send as help answer.
Arguments are
my ($self, $transaction, @args) = @_;
with C<@args> being the arguments from the client's command.
=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_noop
If the client sents the B<NOOP> command, this hook is called. Default is to
return C<250 OK>.
Allowed return codes are:
=over 4
=item DONE
Plugin gave the answer
=item DENY_DISCONNECT
Return error code and disconnect client
=item DENY
Return error code.
=item Anything Else...
Give the default answer of B<250 OK>.
=back
Arguments are
my ($self,$transaction,@args) = @_;
=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
See F<docs/authentication.pod>.
=cut
# vim: ts=2 sw=2 expandtab

200
docs/logging.md Normal file
View File

@ -0,0 +1,200 @@
# qpsmtpd logging; user documentation
Qpsmtpd has a modular logging system. Here's a few things you need to know:
* The built-in logging prints log messages to STDERR.
* A variety of logging plugins is included, each with its own behavior.
* When a logging plugin is enabled, the built-in logging is disabled.
* plugins/logging/warn mimics the built-in logging.
* Multiple logging plugins can be enabled simultaneously.
Read the POD within each logging plugin (perldoc plugins/logging/__NAME__)
to learn if it tickles your fancy.
## enabling plugins
To enable logging plugins, edit the file _config/logging_ and uncomment the
entries for the plugins you wish to use.
## logging level
The 'master switch' for loglevel is _config/loglevel_. Qpsmtpd and active
plugins will output all messages that are less than or equal to the value
specified. The log levels correspond to syslog levels:
LOGDEBUG = 7
LOGINFO = 6
LOGNOTICE = 5
LOGWARN = 4
LOGERROR = 3
LOGCRIT = 2
LOGALERT = 1
LOGEMERG = 0
LOGRADAR = 0
Level 6, LOGINFO, is the level at which most servers should start logging. At
level 6, each plugin should log one and occasionally two entries that
summarize their activity. Here's a few sample lines:
(connect) ident::geoip: SA, Saudi Arabia
(connect) ident::p0f: Windows 7 or 8
(connect) earlytalker: pass: remote host said nothing spontaneous
(data_post) domainkeys: skip: unsigned
(data_post) spamassassin: pass, Spam, 21.7 < 100
(data_post) dspam: fail: agree, Spam, 1.00 c
552 we agree, no spam please (#5.6.1)
Three plugins fired during the SMTP connection phase and 3 more ran during the
data\_post phase. Each plugin emitted one entry stating their findings.
If you aren't processing the logs, you can save some disk I/O by reducing the
loglevel, so that the only messages logged are ones that indicate a human
should be taking some corrective action.
## log location
If qpsmtpd is started using the distributed run file (cd ~smtpd; ./run), then
you will see the log entries printed to your terminal. This solution works
great for initial setup and testing and is the simplest case.
A typical way to run qpsmtpd is as a supervised process with daemontools. If
daemontools is already set up, setting up qpsmtpd may be as simple as:
`ln -s /usr/home/smtpd /var/service/`
If svcscan is running, the symlink will be detected and tcpserver will
run the 'run' files in the ./ and ./log directories. Any log entries
emitted will get handled per the instructions in log/run. The default
location specified in log/run is log/main/current.
## plugin loglevel
Most plugins support a loglevel argument after their config/plugins entry.
The value can be a whole number (N) or a relative number (+/-N), where
N is a whole number from 0-7. See the descriptions of each below.
`ident/p0f loglevel 5`
`ident/p0f loglevel -1`
ATTN plugin authors: To support loglevel in your plugin, you must store the
loglevel settings from the plugins/config entry $self->{\_args}{loglevel}. A
simple and recommended example is as follows:
sub register {
my ( $self, $qp ) = (shift, shift);
$self->log(LOGERROR, "Bad arguments") if @_ % 2;
$self->{_args} = { @_ };
}
### whole number
If loglevel is a whole number, then all log activity in the plugin is logged
at that level, regardless of the level the plugin author selected. This can
be easily understood with a couple examples:
The master loglevel is set at 6 (INFO). The mail admin sets a plugin loglevel
to 7 (DEBUG). No messages from that plugin are emitted because DEBUG log
entries are not <= 6 (INFO).
The master loglevel is 6 (INFO) and the plugin loglevel is set to 5 or 6. All
log entries will be logged because 5 is <= 6.
This behavior is very useful to plugin authors. While testing and monitoring
a plugin, they can set the level of their plugin to log everything. To return
to 'normal' logging, they just update their config/plugins entry.
### relative
Relative loglevel arguments adjust the loglevel of each logging call within
a plugin. A value of _loglevel +1_ would make every logging entry one level
less severe, where a value of _loglevel -1_ would make every logging entry
one level more severe.
For example, if a plugin has a loglevel setting of -1 and that same plugin
logged a LOGDEBUG, it would instead be a LOGINFO message. Relative values
makes it easy to control the verbosity and/or severity of individual plugins.
# qpsmtpd logging system; developer documentation
Qpsmtpd now (as of 0.30-dev) supports a plugable logging architecture, so
that different logging plugins can be supported. See the example logging
plugins in plugins/logging, specifically the ["logging/warn" in plugins](https://metacpan.org/pod/plugins#logging-warn) and
["logging/adaptive" in plugins](https://metacpan.org/pod/plugins#logging-adaptive) files for examples of how to write your own
logging plugins.
# plugin authors
While plugins can log anything they like, a few logging conventions in use:
- at LOGINFO, log a single entry summarizing their disposition
- log messages are prefixed with keywords: pass, fail, skip, error
- pass: tests were run and the message passed
- fail: tests were run and the message failed
- fail, tolerated: tests run, msg failed, reject disabled
- skip: tests were not run
- error: tried to run tests but failure(s) encountered
- info: additional info, not to be used for plugin summary
- when tests fail and reject is disabled, use the 'fail, tolerated' prefix
When these conventions are adhered to, the logs/summarize tool outputs each
message as a single row, with a small x showing failed tests and a large X
for failed tests that caused message rejection.
# Internal support for pluggable logging
Any code in the core can call `$self-`log()> and those log lines will be
dispatched to each of the registered logging plugins. When `log()` is
called from a plugin, the plugin and hook names are automatically included
in the parameters passed the logging hooks. All plugins which register for
the logging hook should expect the following parameters to be passed:
$self, $transaction, $trace, $hook, $plugin, @log
where those terms are:
- `$self`
The object which was used to call the log() method; this can be any object
within the system, since the core code will automatically load logging
plugins on behalf of any object.
- `$transaction`
This is the current SMTP transaction (defined as everything that happens
between HELO/EHLO and QUIT/RSET). If you want to defer outputting certain
log lines, you can store them in the transaction object, but you will need
to bind the `reset_transaction` hook in order to retrieve that information
before it is discarded when the transaction is closed (see the
["adaptive" in logging](https://metacpan.org/pod/logging#adaptive) plugin for an example of doing this).
- `$trace`
This is the log level (as shown in config.sample/loglevel) that the caller
asserted when calling log(). If you want to output the textural
representation (e.g. `LOGERROR`) of this in your log messages, you can use
the log\_level() function exported by Qpsmtpd::Constants (which is
automatically available to all plugins).
- `$hook`
This is the hook that is currently being executed. If log() is called by
any core code (i.e. not as part of a hook), this term will be `undef`.
- `$plugin`
This is the plugin name that executed the log(). Like `$hook`, if part of
the core code calls log(), this wil be `undef`. See ["warn" in logging](https://metacpan.org/pod/logging#warn) for a
way to prevent logging your own plugin's log entries from within that
plugin (the system will not infinitely recurse in any case).
- `@log`
The remaining arguments are as passed by the caller, which may be a single
term or may be a list of values. It is usually sufficient to call
`join(" ",@log)` to deal with these terms, but it is possible that some
plugin might pass additional arguments with signficance.
Note: if you register a handler for certain hooks, e.g. `deny`, there may
be additional terms passed between `$self` and `$transaction`. See
["adaptive" in logging](https://metacpan.org/pod/logging#adaptive) for and example.

View File

@ -1,225 +0,0 @@
#
# read this with 'perldoc docs/logging.pod'
#
=head1 qpsmtpd logging; user documentation
Qpsmtpd has a modular logging system. Here's a few things you need to know:
* The built-in logging prints log messages to STDERR.
* A variety of logging plugins is included, each with its own behavior.
* When a logging plugin is enabled, the built-in logging is disabled.
* plugins/logging/warn mimics the built-in logging.
* Multiple logging plugins can be enabled simultaneously.
Read the POD within each logging plugin (perldoc plugins/logging/B<NAME>)
to learn if it tickles your fancy.
=head2 enabling plugins
To enable logging plugins, edit the file I<config/logging> and uncomment the
entries for the plugins you wish to use.
=head2 logging level
The 'master switch' for loglevel is I<config/loglevel>. Qpsmtpd and active
plugins will output all messages that are less than or equal to the value
specified. The log levels correspond to syslog levels:
LOGDEBUG = 7
LOGINFO = 6
LOGNOTICE = 5
LOGWARN = 4
LOGERROR = 3
LOGCRIT = 2
LOGALERT = 1
LOGEMERG = 0
LOGRADAR = 0
Level 6, LOGINFO, is the level at which most servers should start logging. At
level 6, each plugin should log one and occasionally two entries that
summarize their activity. Here's a few sample lines:
(connect) ident::geoip: SA, Saudi Arabia
(connect) ident::p0f: Windows 7 or 8
(connect) earlytalker: pass: remote host said nothing spontaneous
(data_post) domainkeys: skip: unsigned
(data_post) spamassassin: pass, Spam, 21.7 < 100
(data_post) dspam: fail: agree, Spam, 1.00 c
552 we agree, no spam please (#5.6.1)
Three plugins fired during the SMTP connection phase and 3 more ran during the
data_post phase. Each plugin emitted one entry stating their findings.
If you aren't processing the logs, you can save some disk I/O by reducing the
loglevel, so that the only messages logged are ones that indicate a human
should be taking some corrective action.
=head2 log location
If qpsmtpd is started using the distributed run file (cd ~smtpd; ./run), then
you will see the log entries printed to your terminal. This solution works
great for initial setup and testing and is the simplest case.
A typical way to run qpsmtpd is as a supervised process with daemontools. If
daemontools is already set up, setting up qpsmtpd may be as simple as:
C<ln -s /usr/home/smtpd /var/service/>
If svcscan is running, the symlink will be detected and tcpserver will
run the 'run' files in the ./ and ./log directories. Any log entries
emitted will get handled per the instructions in log/run. The default
location specified in log/run is log/main/current.
=head2 plugin loglevel
Most plugins support a loglevel argument after their config/plugins entry.
The value can be a whole number (N) or a relative number (+/-N), where
N is a whole number from 0-7. See the descriptions of each below.
C<ident/p0f loglevel 5>
C<ident/p0f loglevel -1>
ATTN plugin authors: To support loglevel in your plugin, you must store the
loglevel settings from the plugins/config entry $self->{_args}{loglevel}. A
simple and recommended example is as follows:
sub register {
my ( $self, $qp ) = (shift, shift);
$self->log(LOGERROR, "Bad arguments") if @_ % 2;
$self->{_args} = { @_ };
}
=head3 whole number
If loglevel is a whole number, then all log activity in the plugin is logged
at that level, regardless of the level the plugin author selected. This can
be easily understood with a couple examples:
The master loglevel is set at 6 (INFO). The mail admin sets a plugin loglevel
to 7 (DEBUG). No messages from that plugin are emitted because DEBUG log
entries are not <= 6 (INFO).
The master loglevel is 6 (INFO) and the plugin loglevel is set to 5 or 6. All
log entries will be logged because 5 is <= 6.
This behavior is very useful to plugin authors. While testing and monitoring
a plugin, they can set the level of their plugin to log everything. To return
to 'normal' logging, they just update their config/plugins entry.
=head3 relative
Relative loglevel arguments adjust the loglevel of each logging call within
a plugin. A value of I<loglevel +1> would make every logging entry one level
less severe, where a value of I<loglevel -1> would make every logging entry
one level more severe.
For example, if a plugin has a loglevel setting of -1 and that same plugin
logged a LOGDEBUG, it would instead be a LOGINFO message. Relative values
makes it easy to control the verbosity and/or severity of individual plugins.
=head1 qpsmtpd logging system; developer documentation
Qpsmtpd now (as of 0.30-dev) supports a plugable logging architecture, so
that different logging plugins can be supported. See the example logging
plugins in plugins/logging, specifically the L<plugins/logging/warn> and
L<plugins/logging/adaptive> files for examples of how to write your own
logging plugins.
=head1 plugin authors
While plugins can log anything they like, a few logging conventions in use:
=over 4
=item * at LOGINFO, log a single entry summarizing their disposition
=item * log messages are prefixed with keywords: pass, fail, skip, error
=over 4
=item pass: tests were run and the message passed
=item fail: tests were run and the message failed
=item fail, tolerated: tests run, msg failed, reject disabled
=item skip: tests were not run
=item error: tried to run tests but failure(s) encountered
=item info: additional info, not to be used for plugin summary
=back
=item * when tests fail and reject is disabled, use the 'fail, tolerated' prefix
=back
When these conventions are adhered to, the logs/summarize tool outputs each
message as a single row, with a small x showing failed tests and a large X
for failed tests that caused message rejection.
=head1 Internal support for pluggable logging
Any code in the core can call C<$self->log()> and those log lines will be
dispatched to each of the registered logging plugins. When C<log()> is
called from a plugin, the plugin and hook names are automatically included
in the parameters passed the logging hooks. All plugins which register for
the logging hook should expect the following parameters to be passed:
$self, $transaction, $trace, $hook, $plugin, @log
where those terms are:
=over 4
=item C<$self>
The object which was used to call the log() method; this can be any object
within the system, since the core code will automatically load logging
plugins on behalf of any object.
=item C<$transaction>
This is the current SMTP transaction (defined as everything that happens
between HELO/EHLO and QUIT/RSET). If you want to defer outputting certain
log lines, you can store them in the transaction object, but you will need
to bind the C<reset_transaction> hook in order to retrieve that information
before it is discarded when the transaction is closed (see the
L<logging/adaptive> plugin for an example of doing this).
=item C<$trace>
This is the log level (as shown in config.sample/loglevel) that the caller
asserted when calling log(). If you want to output the textural
representation (e.g. C<LOGERROR>) of this in your log messages, you can use
the log_level() function exported by Qpsmtpd::Constants (which is
automatically available to all plugins).
=item C<$hook>
This is the hook that is currently being executed. If log() is called by
any core code (i.e. not as part of a hook), this term will be C<undef>.
=item C<$plugin>
This is the plugin name that executed the log(). Like C<$hook>, if part of
the core code calls log(), this wil be C<undef>. See L<logging/warn> for a
way to prevent logging your own plugin's log entries from within that
plugin (the system will not infinitely recurse in any case).
=item C<@log>
The remaining arguments are as passed by the caller, which may be a single
term or may be a list of values. It is usually sufficient to call
C<join(" ",@log)> to deal with these terms, but it is possible that some
plugin might pass additional arguments with signficance.
=back
Note: if you register a handler for certain hooks, e.g. C<deny>, there may
be additional terms passed between C<$self> and C<$transaction>. See
L<logging/adaptive> for and example.

View File

@ -1,395 +0,0 @@
#
# This file is best read with ``perldoc plugins.pod''
#
###
# Conventions:
# plugin names: F<myplugin>
# 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<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;>, see
L</Hook - Subroutine translations> for more info. 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 the pperl flavor 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 Hook - Subroutine translations
As mentioned above, the hook name needs to be translated to a valid perl
C<sub> name. This is done like
($sub = $hook) =~ s/\W/_/g;
$sub = "hook_$sub";
Some examples follow, for a complete list of available (documented ;-))
hooks (method names), use something like
$ perl -lne 'print if s/^=head2\s+(hook_\S+)/$1/' docs/plugins.pod
All valid hooks are defined in F<lib/Qpsmtpd/Plugins.pm>, C<our @hooks>.
=head3 Translation table
hook method
---------- ------------
config hook_config
queue hook_queue
data hook_data
data_post hook_data_post
quit hook_quit
rcpt hook_rcpt
mail hook_mail
ehlo hook_ehlo
helo hook_helo
auth hook_auth
auth-plain hook_auth_plain
auth-login hook_auth_login
auth-cram-md5 hook_auth_cram_md5
connect hook_connect
reset_transaction hook_reset_transaction
unrecognized_command hook_unrecognized_command
=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);
}
See also chapter C<Changing return values> and
F<contrib/vetinari/rcpt_ok_maxrelay> in SVN.
=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->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, set your plugins loglevel to LOGDEBUG. This
will log every logging statement within your plugin.
For more information about logging, see F<docs/logging.pod>.
=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
=cut

232
docs/writing.md Normal file
View File

@ -0,0 +1,232 @@
# 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 `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 _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);
- The client starts with a safe 8-bit text message. It encodes the message
as the byte string `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 [http://cr.yp.to/proto/qmqp.html](http://cr.yp.to/proto/qmqp.html))
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
`Text::Netstring` module if you know the size of the string (for more
info about netstrings see [http://cr.yp.to/proto/netstrings.txt](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...
- (continued from [http://cr.yp.to/proto/qmqp.html](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.
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 `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;
}
}

View File

@ -1,271 +0,0 @@
#
# This file is best read with ``perldoc writing.pod''
#
###
# Conventions:
# plugin names: F<myplugin>
# 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 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;
}
}
=cut
# vim: ts=2 sw=2 expandtab