convert docs/* from POD to Markdown
1. easier to update 2. it's readable on github
This commit is contained in:
parent
5c9e7631ed
commit
a1823c8f50
@ -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
333
README.plugins.md
Normal 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.
|
70
docs/advanced.md
Normal file
70
docs/advanced.md
Normal 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.
|
@ -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
231
docs/authentication.md
Normal 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.
|
@ -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
183
docs/config.md
Normal 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.
|
200
docs/config.pod
200
docs/config.pod
@ -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
|
||||
|
@ -1,114 +1,113 @@
|
||||
# Developing Qpsmtpd
|
||||
|
||||
=head1 Developing Qpsmtpd
|
||||
|
||||
=head2 Mailing List
|
||||
## Mailing List
|
||||
|
||||
All qpsmtpd development happens on the qpsmtpd mailing list.
|
||||
|
||||
Subscribe by sending mail to qpsmtpd-subscribe@perl.org
|
||||
|
||||
=head2 Git
|
||||
## Git
|
||||
|
||||
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
|
||||
changes easily accessible for pulling into the master. After you
|
||||
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.
|
||||
|
||||
=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
|
||||
|
||||
=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
|
||||
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
|
||||
|
||||
git checkout -b topic/my-great-change
|
||||
git checkout -b 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).
|
||||
|
||||
=head3 Committing a change
|
||||
### Committing a change
|
||||
|
||||
Edit the appropriate files, and be sure to run the test suite.
|
||||
|
||||
emacs lib/Qpsmtpd.pm # for example
|
||||
perl Makefile.PL
|
||||
make test
|
||||
emacs lib/Qpsmtpd.pm # for example
|
||||
perl Makefile.PL
|
||||
make test
|
||||
|
||||
When you're ready to check it in...
|
||||
|
||||
git add lib/Qpsmtpd.pm # to let git know you changed the file
|
||||
git add --patch plugin/tls # interactive choose which changes to add
|
||||
git diff --cached # review changes added
|
||||
git commit # describe the commit
|
||||
git log -p # review your commit a last time
|
||||
git push origin # to send to github
|
||||
git add lib/Qpsmtpd.pm # to let git know you changed the file
|
||||
git add --patch plugin/tls # interactive choose which changes to add
|
||||
git diff --cached # review changes added
|
||||
git commit # describe the commit
|
||||
git log -p # review your commit a last time
|
||||
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
|
||||
a single short (less than 50 character) line summarizing the change,
|
||||
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
|
||||
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
|
||||
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:
|
||||
|
||||
git format-patch HEAD~3
|
||||
git format-patch HEAD~3
|
||||
|
||||
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).
|
||||
|
||||
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
|
||||
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'
|
||||
for now, but you could call it anything you want. You only have to do
|
||||
this once.
|
||||
|
||||
git remote add smtpd git://github.com/smtpd/qpsmtpd.git
|
||||
git remote add smtpd git://github.com/smtpd/qpsmtpd.git
|
||||
|
||||
Pull in data from all remote branches
|
||||
|
||||
git remote update
|
||||
git remote update
|
||||
|
||||
Forward-port local commits to the updated upstream head
|
||||
|
||||
git rebase smtpd/master
|
||||
git rebase smtpd/master
|
||||
|
||||
If you have a change that conflicts with an upstream change (git will
|
||||
let you know) you have two options.
|
||||
let you know) you have two options.
|
||||
|
||||
Manually fix the conflict and then do
|
||||
Manually fix the conflict and then do
|
||||
|
||||
git add some/file
|
||||
git commit
|
||||
git add some/file
|
||||
git commit
|
||||
|
||||
Or if the conflicting upstream commit did the same logical change then
|
||||
you might want to just skip the local change:
|
||||
|
||||
git rebase --skip
|
||||
git rebase --skip
|
||||
|
||||
Be sure to decide whether you're going to skip before you merge, or
|
||||
you might get yourself into an odd situation.
|
||||
@ -116,28 +115,27 @@ you might get yourself into an odd situation.
|
||||
Conflicts happen because upstream committers may make minor tweaks to
|
||||
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
|
||||
always revert to the last commit:
|
||||
|
||||
git reset --hard HEAD
|
||||
git reset --hard HEAD
|
||||
|
||||
Or throw away your most recent commit:
|
||||
|
||||
git reset --hard HEAD^
|
||||
git reset --hard HEAD^
|
||||
|
||||
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
|
||||
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
|
||||
other peoples changes is to use C<git am>. That will go ahead and
|
||||
commit the change. To modify it, you can use C<git commit --amend>.
|
||||
other peoples changes is to use `git am`. That will go ahead and
|
||||
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
|
||||
"git remote add" and then either merge them in with "git merge" or
|
||||
pick just the relevant commits with "git cherry-pick".
|
||||
|
778
docs/hooks.md
Normal file
778
docs/hooks.md
Normal 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`.
|
928
docs/hooks.pod
928
docs/hooks.pod
@ -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
200
docs/logging.md
Normal 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.
|
225
docs/logging.pod
225
docs/logging.pod
@ -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.
|
||||
|
395
docs/plugins.pod
395
docs/plugins.pod
@ -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
232
docs/writing.md
Normal 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;
|
||||
}
|
||||
}
|
271
docs/writing.pod
271
docs/writing.pod
@ -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
|
Loading…
Reference in New Issue
Block a user