Merge pull request #117 from msimerson/docs-to-md
docs/* from POD -> Markdown
This commit is contained in:
commit
92fadd703a
20
MANIFEST
20
MANIFEST
@ -25,14 +25,13 @@ config.sample/smtpauth-checkpassword
|
|||||||
config.sample/tls_before_auth
|
config.sample/tls_before_auth
|
||||||
config.sample/tls_ciphers
|
config.sample/tls_ciphers
|
||||||
CREDITS
|
CREDITS
|
||||||
docs/advanced.pod
|
docs/advanced.md
|
||||||
docs/authentication.pod
|
docs/authentication.md
|
||||||
docs/config.pod
|
docs/config.md
|
||||||
docs/development.pod
|
docs/development.md
|
||||||
docs/hooks.pod
|
docs/hooks.md
|
||||||
docs/logging.pod
|
docs/logging.md
|
||||||
docs/plugins.pod
|
docs/writing.md
|
||||||
docs/writing.pod
|
|
||||||
lib/Apache/Qpsmtpd.pm
|
lib/Apache/Qpsmtpd.pm
|
||||||
lib/Qpsmtpd.pm
|
lib/Qpsmtpd.pm
|
||||||
lib/Qpsmtpd/Address.pm
|
lib/Qpsmtpd/Address.pm
|
||||||
@ -147,9 +146,8 @@ plugins/whitelist
|
|||||||
qpsmtpd
|
qpsmtpd
|
||||||
qpsmtpd-forkserver
|
qpsmtpd-forkserver
|
||||||
qpsmtpd-prefork
|
qpsmtpd-prefork
|
||||||
README
|
|
||||||
README.md
|
README.md
|
||||||
README.plugins
|
README.plugins.md
|
||||||
run.forkserver
|
run.forkserver
|
||||||
run.tcpserver
|
run.tcpserver
|
||||||
STATUS
|
STATUS
|
||||||
@ -207,6 +205,6 @@ t/qpsmtpd.t
|
|||||||
t/rset.t
|
t/rset.t
|
||||||
t/Test/Qpsmtpd.pm
|
t/Test/Qpsmtpd.pm
|
||||||
t/Test/Qpsmtpd/Plugin.pm
|
t/Test/Qpsmtpd/Plugin.pm
|
||||||
UPGRADING.pod
|
UPGRADING.md
|
||||||
xt/01-syntax.t
|
xt/01-syntax.t
|
||||||
xt/02-pod.t
|
xt/02-pod.t
|
||||||
|
199
README
199
README
@ -1,199 +0,0 @@
|
|||||||
#
|
|
||||||
# this file is best read with `perldoc README`
|
|
||||||
#
|
|
||||||
|
|
||||||
=head1 NAME
|
|
||||||
|
|
||||||
Qpsmtpd - qmail perl simple mail transfer protocol daemon
|
|
||||||
|
|
||||||
web:
|
|
||||||
http://smtpd.github.io/qpsmtpd/
|
|
||||||
|
|
||||||
mailinglist:
|
|
||||||
qpsmtpd-subscribe@perl.org
|
|
||||||
|
|
||||||
FAQ:
|
|
||||||
https://github.com/smtpd/qpsmtpd/wiki/faq
|
|
||||||
|
|
||||||
=head1 DESCRIPTION
|
|
||||||
|
|
||||||
What is Qpsmtpd?
|
|
||||||
|
|
||||||
Qpsmtpd is an extensible SMTP engine written in Perl. No, make that
|
|
||||||
easily extensible! See plugins/quit_fortune for a very useful, er,
|
|
||||||
cute example.
|
|
||||||
|
|
||||||
=head2 License
|
|
||||||
|
|
||||||
Qpsmtpd is licensed under the MIT License; see the LICENSE file for
|
|
||||||
more information.
|
|
||||||
|
|
||||||
=head2 What's new in this release?
|
|
||||||
|
|
||||||
See the Changes file! :-)
|
|
||||||
|
|
||||||
|
|
||||||
=head1 Installation
|
|
||||||
|
|
||||||
=head2 Required Perl Modules
|
|
||||||
|
|
||||||
The following Perl modules are required:
|
|
||||||
Net::DNS
|
|
||||||
MIME::Base64
|
|
||||||
Mail::Header (part of the MailTools distribution)
|
|
||||||
|
|
||||||
If you use a version of Perl older than 5.8.0 you will also need
|
|
||||||
Data::Dumper
|
|
||||||
File::Temp
|
|
||||||
Time::HiRes
|
|
||||||
|
|
||||||
The easiest way to install modules from CPAN is with the CPAN shell.
|
|
||||||
Run it with
|
|
||||||
|
|
||||||
perl -MCPAN -e shell
|
|
||||||
|
|
||||||
=head2 qpsmtpd installation
|
|
||||||
|
|
||||||
Make a new user and a directory where you'll install qpsmtpd. I
|
|
||||||
usually use "smtpd" for the user and /home/smtpd/qpsmtpd/ for the
|
|
||||||
directory.
|
|
||||||
|
|
||||||
Put the files there. If you install from git you can just do
|
|
||||||
run the following command in the /home/smtpd/ directory.
|
|
||||||
|
|
||||||
git clone git://github.com/smtpd/qpsmtpd.git
|
|
||||||
|
|
||||||
Beware that the master branch might be unstable and unsuitable for anything
|
|
||||||
but development, so you might want to get a specific release, for
|
|
||||||
example (after running git clone):
|
|
||||||
|
|
||||||
git checkout -b local_branch v0.93
|
|
||||||
|
|
||||||
chmod o+t ~smtpd/qpsmtpd/ (or whatever directory you installed qpsmtpd
|
|
||||||
in) to make supervise start the log process.
|
|
||||||
|
|
||||||
Edit the file config/IP and put the ip address you want to use for
|
|
||||||
qpsmtpd on the first line (or use 0 to bind to all interfaces).
|
|
||||||
|
|
||||||
If you use the supervise tools, then you are practically done!
|
|
||||||
Just symlink /home/smtpd/qpsmtpd into your /services (or /var/services
|
|
||||||
or /var/svscan or whatever) directory. Remember to shutdown
|
|
||||||
qmail-smtpd if you are replacing it with qpsmtpd.
|
|
||||||
|
|
||||||
If you don't use supervise, then you need to run the ./run script in
|
|
||||||
some other way.
|
|
||||||
|
|
||||||
The smtpd user needs write access to ~smtpd/qpsmtpd/tmp/ but should
|
|
||||||
not need to write anywhere else. This directory can be configured
|
|
||||||
with the "spool_dir" configuration and permissions can be set with
|
|
||||||
"spool_perms".
|
|
||||||
|
|
||||||
As per version 0.25 the distributed ./run script runs tcpserver with
|
|
||||||
the -R flag to disable identd lookups. Remove the -R flag if that's
|
|
||||||
not what you want.
|
|
||||||
|
|
||||||
|
|
||||||
=head2 Configuration
|
|
||||||
|
|
||||||
Configuration files can go into either /var/qmail/control or into the
|
|
||||||
config subdirectory of the qpsmtpd installation. Configuration should
|
|
||||||
be compatible with qmail-smtpd making qpsmtpd a drop-in replacement.
|
|
||||||
|
|
||||||
If qmail is installed in a nonstandard location you should set the
|
|
||||||
$QMAIL environment variable to that location in your "./run" file.
|
|
||||||
|
|
||||||
If there is anything missing, then please send a patch (or just
|
|
||||||
information about what's missing) to the mailinglist or a PR to github.
|
|
||||||
|
|
||||||
|
|
||||||
=head1 Better Performance
|
|
||||||
|
|
||||||
For better performance we recommend using "qpsmtpd-forkserver" or
|
|
||||||
running qpsmtpd under Apache 2.x. If you need extremely high
|
|
||||||
concurrency use http://haraka.github.io/
|
|
||||||
|
|
||||||
=head1 Plugins
|
|
||||||
|
|
||||||
The qpsmtpd core only implements the SMTP protocol. No useful
|
|
||||||
function can be done by qpsmtpd without loading plugins.
|
|
||||||
|
|
||||||
Plugins are loaded on startup where each of them register their
|
|
||||||
interest in various "hooks" provided by the qpsmtpd core engine.
|
|
||||||
|
|
||||||
At least one plugin MUST allow or deny the RCPT command to enable
|
|
||||||
receiving mail. The "rcpt_ok" is one basic plugin that does
|
|
||||||
this. Other plugins provide extra functionality related to this; for
|
|
||||||
example the resolvable_fromhost plugin described above.
|
|
||||||
|
|
||||||
|
|
||||||
=head1 Configuration files
|
|
||||||
|
|
||||||
All the files used by qmail-smtpd should be supported; so see the man
|
|
||||||
page for qmail-smtpd. Extra files used by qpsmtpd include:
|
|
||||||
|
|
||||||
=over 4
|
|
||||||
|
|
||||||
=item plugins
|
|
||||||
|
|
||||||
List of plugins, one per line, to be loaded in the order they
|
|
||||||
appear in the file. Plugins are in the plugins directory (or in
|
|
||||||
a subdirectory of there).
|
|
||||||
|
|
||||||
|
|
||||||
=item rhsbl_zones
|
|
||||||
|
|
||||||
Right hand side blocking lists, one per line. For example:
|
|
||||||
|
|
||||||
dsn.rfc-ignorant.org does not accept bounces - http://www.rfc-ignorant.org/
|
|
||||||
|
|
||||||
See http://www.rfc-ignorant.org/ for more examples.
|
|
||||||
|
|
||||||
|
|
||||||
=item dnsbl_zones
|
|
||||||
|
|
||||||
Normal ip based DNS blocking lists ("RBLs"). For example:
|
|
||||||
|
|
||||||
relays.ordb.org
|
|
||||||
spamsources.fabel.dk
|
|
||||||
|
|
||||||
|
|
||||||
=item spool_dir
|
|
||||||
|
|
||||||
If this file contains a directory, it will be the spool directory
|
|
||||||
smtpd uses during the data transactions. If this file doesn't exist, it
|
|
||||||
will default to use $ENV{HOME}/tmp/. This directory should be set with
|
|
||||||
a mode of 700 and owned by the smtpd user.
|
|
||||||
|
|
||||||
=item spool_perms
|
|
||||||
|
|
||||||
The default spool permissions are 0700. If you need some other value,
|
|
||||||
chmod the directory and set it's octal value in config/spool_perms.
|
|
||||||
|
|
||||||
=item tls_before_auth
|
|
||||||
|
|
||||||
If this file contains anything except a 0 on the first noncomment line, then
|
|
||||||
AUTH will not be offered unless TLS/SSL are in place, either with STARTTLS,
|
|
||||||
or SMTP-SSL on port 465.
|
|
||||||
|
|
||||||
=item everything (?) that qmail-smtpd supports.
|
|
||||||
|
|
||||||
In my test qpsmtpd installation I have a "config/me" file containing
|
|
||||||
the hostname I use for testing qpsmtpd (so it doesn't introduce itself
|
|
||||||
with the normal name of the server).
|
|
||||||
|
|
||||||
=back
|
|
||||||
|
|
||||||
|
|
||||||
=head1 Problems
|
|
||||||
|
|
||||||
In case of problems, always check the logfile first.
|
|
||||||
|
|
||||||
By default, qpsmtpd logs to log/main/current. Qpsmtpd can log a lot of
|
|
||||||
debug information. You can get more or less by adjusting the number in
|
|
||||||
config/loglevel. Between 1 and 3 should give you a little. Setting it
|
|
||||||
to 10 or higher will get lots of information in the logs.
|
|
||||||
|
|
||||||
If the logfile doesn't give away the problem, then post to the
|
|
||||||
mailinglist (subscription instructions above). If possible, put
|
|
||||||
the logfile on a webserver and include a reference to it in the mail.
|
|
||||||
|
|
@ -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.
|
@ -1,22 +1,21 @@
|
|||||||
|
# Upgrade notes
|
||||||
=head1 Upgrade notes
|
|
||||||
|
|
||||||
When upgrading please review these notes for the versions you are
|
When upgrading please review these notes for the versions you are
|
||||||
upgrading I<from>.
|
upgrading _from_.
|
||||||
|
|
||||||
=head2 v0.84 or below
|
## v0.84 or below
|
||||||
|
|
||||||
=head3 CHECK_RELAY, CHECK_NORELAY, RELAY_ONLY
|
### CHECK\_RELAY, CHECK\_NORELAY, RELAY\_ONLY
|
||||||
|
|
||||||
All 3 plugins are deprecated and replaced with a new 'relay'
|
All 3 plugins are deprecated and replaced with a new 'relay'
|
||||||
plugin. The new plugin reads the same config files (see 'perldoc
|
plugin. The new plugin reads the same config files (see 'perldoc
|
||||||
plugins/relay') as the previous plugins. To get the equivalent
|
plugins/relay') as the previous plugins. To get the equivalent
|
||||||
functionality of enabling 'relay_only', use the 'only' argument to the
|
functionality of enabling 'relay\_only', use the 'only' argument to the
|
||||||
relay plugin as documented in the RELAY ONLY section of plugins/relay.
|
relay plugin as documented in the RELAY ONLY section of plugins/relay.
|
||||||
|
|
||||||
=head3 GREYLISTING plugin
|
### GREYLISTING plugin
|
||||||
|
|
||||||
'mode' config argument is deprecated. Use reject and reject_type instead.
|
'mode' config argument is deprecated. Use reject and reject\_type instead.
|
||||||
|
|
||||||
The greylisting DB format has changed to accommodate IPv6
|
The greylisting DB format has changed to accommodate IPv6
|
||||||
addresses. (The DB key has colon ':' seperated fields, and IPv6
|
addresses. (The DB key has colon ':' seperated fields, and IPv6
|
||||||
@ -28,19 +27,16 @@ qpsmtpd once, make one connection. A log entry will be made, telling
|
|||||||
how many records were upgraded. Remove the upgrade option from your
|
how many records were upgraded. Remove the upgrade option from your
|
||||||
config.
|
config.
|
||||||
|
|
||||||
=head3 SPF plugin
|
### SPF plugin
|
||||||
|
|
||||||
spf_deny setting deprecated. Use reject N setting instead, which
|
spf\_deny setting deprecated. Use reject N setting instead, which
|
||||||
provides administrators with more granular control over SPF. For
|
provides administrators with more granular control over SPF. For
|
||||||
backward compatibility, a spf_deny setting of 1 is mapped to 'reject
|
backward compatibility, a spf\_deny setting of 1 is mapped to 'reject
|
||||||
3' and a 'spf_deny 2' is mapped to 'reject 4'.
|
3' and a 'spf\_deny 2' is mapped to 'reject 4'.
|
||||||
|
|
||||||
|
### P0F plugin
|
||||||
=head3 P0F plugin
|
|
||||||
|
|
||||||
defaults to p0f v3 (was v2).
|
defaults to p0f v3 (was v2).
|
||||||
|
|
||||||
Upgrade p0f to version 3 or add 'version 2' to your p0f line in
|
Upgrade p0f to version 3 or add 'version 2' to your p0f line in
|
||||||
config/plugins. perldoc plugins/ident/p0f for more details.
|
config/plugins. perldoc plugins/ident/p0f for more details.
|
||||||
|
|
||||||
|
|
70
docs/advanced.md
Normal file
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
|
## Mailing List
|
||||||
|
|
||||||
=head2 Mailing List
|
|
||||||
|
|
||||||
All qpsmtpd development happens on the qpsmtpd mailing list.
|
All qpsmtpd development happens on the qpsmtpd mailing list.
|
||||||
|
|
||||||
Subscribe by sending mail to qpsmtpd-subscribe@perl.org
|
Subscribe by sending mail to qpsmtpd-subscribe@perl.org
|
||||||
|
|
||||||
=head2 Git
|
## Git
|
||||||
|
|
||||||
We use git for version control.
|
We use git for version control.
|
||||||
|
|
||||||
Ask owns the master repository at git://github.com/smtpd/qpsmtpd.git
|
The master repository is at git://github.com/smtpd/qpsmtpd.git
|
||||||
|
|
||||||
We suggest using github to host your repository -- it makes your
|
We suggest using github to host your repository -- it makes your
|
||||||
changes easily accessible for pulling into the master. After you
|
changes easily accessible for pulling into the master. After you
|
||||||
create a github account, go to
|
create a github account, go to
|
||||||
http://github.com/smtpd/qpsmtpd/tree/master and click on the "fork"
|
[the master repository](http://github.com/smtpd/qpsmtpd/tree/master) and click on the "fork"
|
||||||
button to get your own repository.
|
button to get your own repository.
|
||||||
|
|
||||||
=head3 Making a working Copy
|
### Making a working Copy
|
||||||
|
|
||||||
git clone git@github.com:username/qpsmtpd.git qpsmtpd
|
git clone git@github.com:username/qpsmtpd.git qpsmtpd
|
||||||
|
|
||||||
will check out your copy into a directory called qpsmtpd
|
will check out your copy into a directory called qpsmtpd
|
||||||
|
|
||||||
=head3 Making a branch for your change
|
### Making a branch for your change
|
||||||
|
|
||||||
As a general rule, you'll be better off if you do your changes on a
|
As a general rule, you'll be better off if you do your changes on a
|
||||||
branch - preferably a branch per unrelated change.
|
branch - preferably a branch per unrelated change.
|
||||||
|
|
||||||
You can use the C<git branch> command to see which branch you are on.
|
You can use the `git branch` command to see which branch you are on.
|
||||||
|
|
||||||
The easiest way to make a new branch is
|
The easiest way to make a new branch is
|
||||||
|
|
||||||
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"
|
This will create a new branch with the name "topic/my-great-change"
|
||||||
(and your current commit as the starting point).
|
(and your current commit as the starting point).
|
||||||
|
|
||||||
=head3 Committing a change
|
### Committing a change
|
||||||
|
|
||||||
Edit the appropriate files, and be sure to run the test suite.
|
Edit the appropriate files, and be sure to run the test suite.
|
||||||
|
|
||||||
emacs lib/Qpsmtpd.pm # for example
|
emacs lib/Qpsmtpd.pm # for example
|
||||||
perl Makefile.PL
|
perl Makefile.PL
|
||||||
make test
|
make test
|
||||||
|
|
||||||
When you're ready to check it in...
|
When you're ready to check it in...
|
||||||
|
|
||||||
git add lib/Qpsmtpd.pm # to let git know you changed the file
|
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 add --patch plugin/tls # interactive choose which changes to add
|
||||||
git diff --cached # review changes added
|
git diff --cached # review changes added
|
||||||
git commit # describe the commit
|
git commit # describe the commit
|
||||||
git log -p # review your commit a last time
|
git log -p # review your commit a last time
|
||||||
git push origin # to send to github
|
git push origin # to send to github
|
||||||
|
|
||||||
=head3 Commit Descriptions
|
### Commit Descriptions
|
||||||
|
|
||||||
Though not required, it's a good idea to begin the commit message with
|
Though not required, it's a good idea to begin the commit message with
|
||||||
a single short (less than 50 character) line summarizing the change,
|
a single short (less than 50 character) line summarizing the change,
|
||||||
followed by a blank line and then a more thorough description. Tools
|
followed by a blank line and then a more thorough description. Tools
|
||||||
that turn commits into email, for example, use the first line on the
|
that turn commits into email, for example, use the first line on the
|
||||||
Subject: line and the rest of the commit in the body.
|
Subject: line and the rest of the commit in the body.
|
||||||
(From: L<git-commit(1)>)
|
(From: [git-commit(1)](http://man.he.net/man1/git-commit))
|
||||||
|
|
||||||
=head3 Submit patches by mail
|
### Submit patches by mail
|
||||||
|
|
||||||
The best way to submit patches to the project is to send them to the
|
The best way to submit patches to the project is to send them to the
|
||||||
mailing list for review. Use the C<git format-patch> command to
|
mailing list for review. Use the `git format-patch` command to
|
||||||
generate patches ready to be mailed. For example:
|
generate patches ready to be mailed. For example:
|
||||||
|
|
||||||
git format-patch HEAD~3
|
git format-patch HEAD~3
|
||||||
|
|
||||||
will put each of the last three changes in files ready to be mailed
|
will put each of the last three changes in files ready to be mailed
|
||||||
with the C<git send-email> tool (it might be a good idea to send them
|
with the `git send-email` tool (it might be a good idea to send them
|
||||||
to yourself first as a test).
|
to yourself first as a test).
|
||||||
|
|
||||||
Sending patches to the mailing list is the most effective way to
|
Sending patches to the mailing list is the most effective way to
|
||||||
submit changes, although it helps if you at the same time also commit
|
submit changes, although it helps if you at the same time also commit
|
||||||
them to a git repository (for example on github).
|
them to a git repository (for example on github).
|
||||||
|
|
||||||
=head3 Merging changes back in from the master repository
|
### Merging changes back in from the master repository
|
||||||
|
|
||||||
Tell git about the master repository. We're going to call it 'smtpd'
|
Tell git about the master repository. We're going to call it 'smtpd'
|
||||||
for now, but you could call it anything you want. You only have to do
|
for now, but you could call it anything you want. You only have to do
|
||||||
this once.
|
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
|
Pull in data from all remote branches
|
||||||
|
|
||||||
git remote update
|
git remote update
|
||||||
|
|
||||||
Forward-port local commits to the updated upstream head
|
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
|
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 add some/file
|
||||||
git commit
|
git commit
|
||||||
|
|
||||||
Or if the conflicting upstream commit did the same logical change then
|
Or if the conflicting upstream commit did the same logical change then
|
||||||
you might want to just skip the local change:
|
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
|
Be sure to decide whether you're going to skip before you merge, or
|
||||||
you might get yourself into an odd situation.
|
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
|
Conflicts happen because upstream committers may make minor tweaks to
|
||||||
your change before applying it.
|
your change before applying it.
|
||||||
|
|
||||||
=head3 Throwing away changes
|
### Throwing away changes
|
||||||
|
|
||||||
If you get your working copy into a state you don't like, you can
|
If you get your working copy into a state you don't like, you can
|
||||||
always revert to the last commit:
|
always revert to the last commit:
|
||||||
|
|
||||||
git reset --hard HEAD
|
git reset --hard HEAD
|
||||||
|
|
||||||
Or throw away your most recent commit:
|
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
|
If you make a mistake with this, git is pretty good about keeping your
|
||||||
commits around even as you merge, rebase and reset away. This log of
|
commits around even as you merge, rebase and reset away. This log of
|
||||||
your git changes is called with "git reflog".
|
your git changes is called with "git reflog".
|
||||||
|
|
||||||
=head3 Applying other peoples changes
|
### Applying other peoples changes
|
||||||
|
|
||||||
If you get a change in an email with the patch, one easy way to apply
|
If you get a change in an email with the patch, one easy way to apply
|
||||||
other peoples changes is to use C<git am>. That will go ahead and
|
other peoples changes is to use `git am`. That will go ahead and
|
||||||
commit the change. To modify it, you can use C<git commit --amend>.
|
commit the change. To modify it, you can use `git commit --amend`.
|
||||||
|
|
||||||
If the changes are in a repository, you can add that repository with
|
If the changes are in a repository, you can add that repository with
|
||||||
"git remote add" and then either merge them in with "git merge" or
|
"git remote add" and then either merge them in with "git merge" or
|
||||||
pick just the relevant commits with "git cherry-pick".
|
pick just the relevant commits with "git cherry-pick".
|
||||||
|
|
778
docs/hooks.md
Normal file
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