diff --git a/MANIFEST b/MANIFEST index c810278..4ebdec3 100644 --- a/MANIFEST +++ b/MANIFEST @@ -25,14 +25,13 @@ config.sample/smtpauth-checkpassword config.sample/tls_before_auth config.sample/tls_ciphers CREDITS -docs/advanced.pod -docs/authentication.pod -docs/config.pod -docs/development.pod -docs/hooks.pod -docs/logging.pod -docs/plugins.pod -docs/writing.pod +docs/advanced.md +docs/authentication.md +docs/config.md +docs/development.md +docs/hooks.md +docs/logging.md +docs/writing.md lib/Apache/Qpsmtpd.pm lib/Qpsmtpd.pm lib/Qpsmtpd/Address.pm @@ -147,9 +146,8 @@ plugins/whitelist qpsmtpd qpsmtpd-forkserver qpsmtpd-prefork -README README.md -README.plugins +README.plugins.md run.forkserver run.tcpserver STATUS @@ -207,6 +205,6 @@ t/qpsmtpd.t t/rset.t t/Test/Qpsmtpd.pm t/Test/Qpsmtpd/Plugin.pm -UPGRADING.pod +UPGRADING.md xt/01-syntax.t xt/02-pod.t diff --git a/README b/README deleted file mode 100644 index d00d75a..0000000 --- a/README +++ /dev/null @@ -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. - diff --git a/README.plugins b/README.plugins deleted file mode 100644 index eb02b33..0000000 --- a/README.plugins +++ /dev/null @@ -1,13 +0,0 @@ -# -# read this with 'perldoc README.plugins' ... -# - -=head1 qpsmtpd plugin system; developer documentation - -Plugin documentation is now in F. - -See the examples in plugins/ and ask questions on the qpsmtpd -mailinglist; subscribe by sending mail to qpsmtpd-subscribe@perl.org. - -=cut - diff --git a/README.plugins.md b/README.plugins.md new file mode 100644 index 0000000..330dab7 --- /dev/null +++ b/README.plugins.md @@ -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. diff --git a/UPGRADING.pod b/UPGRADING.md similarity index 68% rename from UPGRADING.pod rename to UPGRADING.md index f2910da..3a117b4 100644 --- a/UPGRADING.pod +++ b/UPGRADING.md @@ -1,22 +1,21 @@ - -=head1 Upgrade notes +# Upgrade notes When upgrading please review these notes for the versions you are -upgrading I. +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' plugin. The new plugin reads the same config files (see 'perldoc 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. -=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 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 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 -backward compatibility, a spf_deny setting of 1 is mapped to 'reject -3' and a 'spf_deny 2' is mapped to 'reject 4'. +backward compatibility, a spf\_deny setting of 1 is mapped to 'reject +3' and a 'spf\_deny 2' is mapped to 'reject 4'. - -=head3 P0F plugin +### P0F plugin defaults to p0f v3 (was v2). Upgrade p0f to version 3 or add 'version 2' to your p0f line in config/plugins. perldoc plugins/ident/p0f for more details. - - diff --git a/docs/advanced.md b/docs/advanced.md new file mode 100644 index 0000000..47bda38 --- /dev/null +++ b/docs/advanced.md @@ -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. diff --git a/docs/advanced.pod b/docs/advanced.pod deleted file mode 100644 index ed3ce5e..0000000 --- a/docs/advanced.pod +++ /dev/null @@ -1,96 +0,0 @@ -# -# This file is best read with ``perldoc advanced.pod'' -# - -### -# Conventions: -# plugin names: F -# constants: I -# smtp commands, answers: B, B<250 Queued!> -# -# Notes: -# * due to restrictions of some POD parsers, no C<<$object->method()>> -# are allowed, use C<$object-Emethod()> -# - -=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, ..., 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 method. - -The B plugin wraps the B plugin. The B -plugin checks the F and F 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 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 configures this plugin to drop -the connection after I unsuccessful relaying attempts. -Set to C<0> to disable, default is C<5>. - -Note: Do not load both (B and B). This plugin -should be configured to run I, like B. - - 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 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: does the above (regarding connection notes) work? - -=cut - -# vim: ts=2 sw=2 expandtab diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..24ff78e --- /dev/null +++ b/docs/authentication.md @@ -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 + +Matt Simerson (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. diff --git a/docs/authentication.pod b/docs/authentication.pod deleted file mode 100644 index fa7373c..0000000 --- a/docs/authentication.pod +++ /dev/null @@ -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 -L - -for more details. - -=head1 USAGE - -This code is automatically loaded by Qpsmtpd::SMTP only if a plugin -providing one of the defined L 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 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 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 -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 - -Matt Simerson (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 diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..a4709e3 --- /dev/null +++ b/docs/config.md @@ -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. diff --git a/docs/config.pod b/docs/config.pod deleted file mode 100644 index 86e0f0b..0000000 --- a/docs/config.pod +++ /dev/null @@ -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, like -qmail's F directory. NB: F (or -F<$ENV{QMAIL}/control>) is used if a file does not exist in C. -The location of the C directory can be set via the -I 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 hook to store the settings in some other -way. See L and L 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 hook: C, C, -C and of course C. B - -=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. - -=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 Iheader, ... -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. Format -is the same as it's used for the F config file. B only -the first non empty line is used (lines starting with C<#> are counted -as empty). - -=item loglevel - -This is not used anymore, I if no F 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. - -=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, default is C<0700>. You probably have to -change the defaults for some scanners (e.g. the F plugin). - -=item timeout - -=item timeoutsmtpd - -Set the timeout for the clients, C is the qmail smtpd control -file, C 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 - -Domains listed in these files will be accepted as valid local domains, -anything else is rejected with a C message. If an entry -in the C 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 and C. -The C file is just checked for exact (case insensitive) -matches. - -=item hosts_allow - -Plugin: F. - -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 - -Allow relaying for hosts listed in this file. The C file accepts -IPs and CIDR entries. The C file accepts IPs and C -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 - -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 and C in the documentation of the -C plugin - -=item resolvable_fromhost - -Plugin: F - -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 config file. The I -expects IP addresses or CIDR (i.e. C 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 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 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 - diff --git a/docs/development.pod b/docs/development.md similarity index 61% rename from docs/development.pod rename to docs/development.md index a77e00e..90bb119 100644 --- a/docs/development.pod +++ b/docs/development.md @@ -1,114 +1,113 @@ +# Developing Qpsmtpd -=head1 Developing Qpsmtpd - -=head2 Mailing List +## Mailing List All qpsmtpd development happens on the qpsmtpd mailing list. Subscribe by sending mail to qpsmtpd-subscribe@perl.org -=head2 Git +## Git We use git for version control. -Ask owns the master repository at git://github.com/smtpd/qpsmtpd.git +The master repository is at git://github.com/smtpd/qpsmtpd.git We suggest using github to host your repository -- it makes your changes easily accessible for pulling into the master. After you create a github account, go to -http://github.com/smtpd/qpsmtpd/tree/master and click on the "fork" +[the master repository](http://github.com/smtpd/qpsmtpd/tree/master) and click on the "fork" button to get your own repository. -=head3 Making a working Copy +### Making a working Copy - git clone git@github.com:username/qpsmtpd.git qpsmtpd + git clone git@github.com:username/qpsmtpd.git qpsmtpd will check out your copy into a directory called qpsmtpd -=head3 Making a branch for your change +### Making a branch for your change As a general rule, you'll be better off if you do your changes on a branch - preferably a branch per unrelated change. -You can use the C command to see which branch you are on. +You can use the `git branch` command to see which branch you are on. -The easiest way to make a new branch is +The easiest way to make a new branch is - git checkout -b topic/my-great-change + git checkout -b topic/my-great-change This will create a new branch with the name "topic/my-great-change" (and your current commit as the starting point). -=head3 Committing a change +### Committing a change Edit the appropriate files, and be sure to run the test suite. - emacs lib/Qpsmtpd.pm # for example - perl Makefile.PL - make test + emacs lib/Qpsmtpd.pm # for example + perl Makefile.PL + make test When you're ready to check it in... - git add lib/Qpsmtpd.pm # to let git know you changed the file - git add --patch plugin/tls # interactive choose which changes to add - git diff --cached # review changes added - git commit # describe the commit - git log -p # review your commit a last time - git push origin # to send to github + git add lib/Qpsmtpd.pm # to let git know you changed the file + git add --patch plugin/tls # interactive choose which changes to add + git diff --cached # review changes added + git commit # describe the commit + git log -p # review your commit a last time + git push origin # to send to github -=head3 Commit Descriptions +### Commit Descriptions Though not required, it's a good idea to begin the commit message with a single short (less than 50 character) line summarizing the change, followed by a blank line and then a more thorough description. Tools that turn commits into email, for example, use the first line on the Subject: line and the rest of the commit in the body. -(From: L) +(From: [git-commit(1)](http://man.he.net/man1/git-commit)) -=head3 Submit patches by mail +### Submit patches by mail The best way to submit patches to the project is to send them to the -mailing list for review. Use the C command to +mailing list for review. Use the `git format-patch` command to generate patches ready to be mailed. For example: - git format-patch HEAD~3 + git format-patch HEAD~3 will put each of the last three changes in files ready to be mailed -with the C tool (it might be a good idea to send them +with the `git send-email` tool (it might be a good idea to send them to yourself first as a test). Sending patches to the mailing list is the most effective way to submit changes, although it helps if you at the same time also commit them to a git repository (for example on github). -=head3 Merging changes back in from the master repository +### Merging changes back in from the master repository Tell git about the master repository. We're going to call it 'smtpd' for now, but you could call it anything you want. You only have to do this once. - git remote add smtpd git://github.com/smtpd/qpsmtpd.git + git remote add smtpd git://github.com/smtpd/qpsmtpd.git Pull in data from all remote branches - git remote update + git remote update Forward-port local commits to the updated upstream head - git rebase smtpd/master + git rebase smtpd/master If you have a change that conflicts with an upstream change (git will -let you know) you have two options. +let you know) you have two options. -Manually fix the conflict and then do +Manually fix the conflict and then do - git add some/file - git commit + git add some/file + git commit Or if the conflicting upstream commit did the same logical change then you might want to just skip the local change: - git rebase --skip + git rebase --skip Be sure to decide whether you're going to skip before you merge, or you might get yourself into an odd situation. @@ -116,28 +115,27 @@ you might get yourself into an odd situation. Conflicts happen because upstream committers may make minor tweaks to your change before applying it. -=head3 Throwing away changes +### Throwing away changes If you get your working copy into a state you don't like, you can always revert to the last commit: - git reset --hard HEAD + git reset --hard HEAD Or throw away your most recent commit: - git reset --hard HEAD^ + git reset --hard HEAD^ If you make a mistake with this, git is pretty good about keeping your commits around even as you merge, rebase and reset away. This log of your git changes is called with "git reflog". -=head3 Applying other peoples changes +### Applying other peoples changes If you get a change in an email with the patch, one easy way to apply -other peoples changes is to use C. That will go ahead and -commit the change. To modify it, you can use C. +other peoples changes is to use `git am`. That will go ahead and +commit the change. To modify it, you can use `git commit --amend`. If the changes are in a repository, you can add that repository with "git remote add" and then either merge them in with "git merge" or pick just the relevant commits with "git cherry-pick". - diff --git a/docs/hooks.md b/docs/hooks.md new file mode 100644 index 0000000..0c74c29 --- /dev/null +++ b/docs/hooks.md @@ -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 `` or `` 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`. diff --git a/docs/hooks.pod b/docs/hooks.pod deleted file mode 100644 index aae4048..0000000 --- a/docs/hooks.pod +++ /dev/null @@ -1,928 +0,0 @@ -# -# This file is best read with ``perldoc plugins.pod'' -# - -### -# Conventions: -# plugin names: F -# constants: I -# smtp commands, answers: B, B<250 Queued!> -# -# Notes: -# * due to restrictions of some POD parsers, no C<<$object->method()>> -# are allowed, use C<$object-Emethod()> -# - -=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 object of the currently -running plugin as the first argument. A C 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: for some hooks (post-fork, post-connection, disconnect, deny, ok) the -return values are ignored. This does B mean you can return anything you -want. It just means the return value is discarded and you can not disconnect -a client with I. The rule to return I to run the -next plugin for this hook (or return I / I 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 and F flavors. - -=cut - -NOT FOR: apache, -server and inetd/pperl - -=pod - -B You should not use this hook to do major work and / or use lookup -methods which (I) take some time, like DNS lookups. This will slow down -B 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 the C<$transaction> is of course C 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 and F. - -=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 in fact you get passed two more arguments, which are C 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 plugin. - -=head2 hook_helo / hook_ehlo - -It is called after the client sent B (hook_ehlo) or B (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/B answer, of course only -if all plugins hooking I return I. - -=back - -Arguments of this hook are - - my ($self, $transaction, $host) = @_; - # $host: the name the client sent in the - # (EH|HE)LO line - -B C<$transaction> is C at this point. - -=head2 hook_mail_pre - -After the B line sent by the client is broken into -pieces by the C, this hook recieves the results. -This hook may be used to pre-accept adresses without the surrounding -IE> (by adding them) or addresses like -Iuser@example.com.E> or Iuser@example.com E> by -removing the trailing I<"."> / C<" ">. - -Expected return values are I and an address which must be parseable -by Cparse()> 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 -command). The plugin gets passed a C 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 According to the SMTP protocol, you can not reject an invalid -sender until after the B stage (except for protocol errors, i.e. -syntax errors in address). So store it in an C<$transaction-Enote()> 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 are F -and F. - -=head2 hook_rcpt_pre - -See C, s/MAIL FROM:/RCPT TO:/. - -=head2 hook_rcpt - -This hook is called after the client sent an I command (after -parsing the line). The given argument is parsed by C, -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 -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. - -=head2 hook_data - -After the client sent the B command, before any data of the message -was sent, this hook is called. - -B This hook, like B, B, B, B, 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 -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 The only real use for I 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. - -=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 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, 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 check arguments - -=head2 hook_data_post - -The C 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 just returning I from a special queue plugin does (nearly) -the same (i.e. dropping the mail to F) 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, F - -=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 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 plugins - -=head2 hook_queue_post - -This hook is called always after C. If the return code is -B I, a message (all remaining return values) with level I -is written to the log. -Arguments are - - my $self = shift; - -B 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 command the first time), -after queueing the mail and every time a client sends a B command. -Arguments are - - my ($self, $transaction) = @_; - -B don't rely on C<$transaction> being valid at this point. - -=head2 hook_quit - -After the client sent a B command, this hook is called (before the -C). - -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 plugin. - -=head2 hook_disconnect - -This hook will be called from several places: After a plugin returned -I, before connection is disconnected or after the -client sent the B command, AFTER the quit hook and ONLY if no plugin -hooking C returned I. - -All return values are ignored, arguments are just C<$self> - -Example plugin is F - -=head2 hook_post_connection - -This is the counter part of the C 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). 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 - -=head1 Parsing Hooks - -Before the line from the client is parsed by -Cparse()> 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 and a reason which is -sent to the client or I and the C<$line> broken into pieces according -to the syntax rules for the command. - -B, the C 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 / B 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 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 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> and -I>, don't add them here, use the C / -C methods for this). All other arguments are -sent to the C hook as B / B parameters (see -RFC 1869 I 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 - -=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-Elog($level, $msg);>. Allowed -return codes are - -=over 4 - -=item DECLINED - -next logging plugin - -=item OK - -(not I, 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 C<$transaction> may be C, depending when / where this hook -is called. It's probably best not to try acessing it. - -All F plugins can be used as example plugins. - -=head2 hook_deny - -This hook is called after a plugin returned I, I, -I or I. All return codes are ignored, -arguments are - - my ($self, $transaction, $prev_plugin, $return, $return_text) = @_; - -B C<$transaction> may be C, depending when / where this hook -is called. It's probably best not to try acessing it. - -Example plugin for this hook is F. - -=head2 hook_ok - -The counter part of C, it is called after a plugin B -return I, I, I or I. -All return codes are ignored, arguments are - - my ( $self, $transaction, $prev_plugin, $return, $return_text ) = @_; - -B C<$transaction> may be C, 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 Cqp-Econfig($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 C<$transaction> may be C, depending when / where this hook -is called. It's probably best not to try acessing it. - -Example plugin is F from the qpsmtpd distribution. - -=head2 hook_user_config - -Called when a per-user configuration directive is requested, for example -if someone calls Cconfig($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 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 C<$transaction> may be C, depending when / where this hook -is called. It's probably best not to try acessing it. - -Example plugin is F. - -=head2 hook_help - -This hook triggers if a client sends the B command, allowed return -codes are: - -=over 4 - -=item DONE - -Plugin gave the answer. - -=item DENY - -The client will get a C 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 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 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 auth_parse - -#=head2 auth - -B - -#=head2 auth-plain - -B - -#=head2 auth-login - -B - -#=head2 auth-cram-md5 - -B - -=pod - -See F. - -=cut - -# vim: ts=2 sw=2 expandtab diff --git a/docs/logging.md b/docs/logging.md new file mode 100644 index 0000000..3bfba41 --- /dev/null +++ b/docs/logging.md @@ -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. diff --git a/docs/logging.pod b/docs/logging.pod deleted file mode 100644 index 0b2495e..0000000 --- a/docs/logging.pod +++ /dev/null @@ -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) -to learn if it tickles your fancy. - -=head2 enabling plugins - -To enable logging plugins, edit the file I and uncomment the -entries for the plugins you wish to use. - -=head2 logging level - -The 'master switch' for loglevel is I. 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 - -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 - -C - -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 would make every logging entry one level -less severe, where a value of I 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 and -L 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 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 hook in order to retrieve that information -before it is discarded when the transaction is closed (see the -L 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) 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. - -=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. See L 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 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, there may -be additional terms passed between C<$self> and C<$transaction>. See -L for and example. - diff --git a/docs/plugins.pod b/docs/plugins.pod deleted file mode 100644 index 666c9ef..0000000 --- a/docs/plugins.pod +++ /dev/null @@ -1,395 +0,0 @@ -# -# This file is best read with ``perldoc plugins.pod'' -# - -### -# Conventions: -# plugin names: F -# constants: I -# smtp commands, answers: B, B<250 Queued!> -# -# Notes: -# * due to restrictions of some POD parsers, no C<<$object->method()>> -# are allowed, use C<$object-Emethod()> -# - -=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 provided by the qpsmtpd core engine. - -At least one plugin B allow or deny the B command to enable -receiving mail. The F plugin is the standard plugin for this. -Other plugins provide extra functionality related to this; for example the -F plugin. - -=head2 Loading Plugins - -The list of plugins to load are configured in the I -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 -are run. The plugins are loaded from the F 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 in the example below. Alternate plugin directories -may be given in the F config file, one directory -per line, these will be searched first before using the builtin fallback -of F relative to the qpsmtpd root directory. It may be -necessary, that the F must be used (if you're using -F, for example). - -Some plugins may be configured by passing arguments in the F -config file. - -A plugin can be loaded two or more times with different arguments by adding -I<:N> to the plugin filename, with I 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 -contain I<::>, e.g. C would be ok, C 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 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 method of the plugin is called, -if present. The arguments passed to C 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 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. - -The next step is to register the hooks the plugin provides. Any method which -is named C 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, see -L 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 method (see below) with the -C 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 method is called last. It receives the same arguments as -C. There is no restriction, what you can do in C, but -creating database connections and reuse them later in the process may not be -a good idea. This initialisation happens before any C 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 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 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, C. - -=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 -plugin or skip a hook with this. Instead of modifying C<@ISA> -directly in your plugin, use the C method from the -C 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 and -F in SVN. - -=head2 Config files - -Most of the existing plugins fetch their configuration data from files in the -F 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 -hook. - -=head2 Logging - -Log messages can be written to the log file (or STDERR if you use the -F 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. - -=head2 Information about the current plugin - -Each plugin inherits the public methods from C. - -=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 object associated with the current -connection - -=item transaction() - -Returns the C 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-Eqp-Etemp_dir()>, which takes a -single parameter for the permissions requested (see L 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 plugin from the qpsmtpd core -distribution. - -The transaction note starts after the B command and are just -valid for the current transaction, see below in the I -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. - -Plugins are run in the order they are listed in the F -configuration file. - -The return constants are defined in C and have -the following meanings: - -=over 4 - -=item DECLINED - -Plugin declined work; proceed as usual. This return code is I -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 diff --git a/docs/writing.md b/docs/writing.md new file mode 100644 index 0000000..cfccb98 --- /dev/null +++ b/docs/writing.md @@ -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; + } + } diff --git a/docs/writing.pod b/docs/writing.pod deleted file mode 100644 index 65352af..0000000 --- a/docs/writing.pod +++ /dev/null @@ -1,271 +0,0 @@ -# -# This file is best read with ``perldoc writing.pod'' -# - -### -# Conventions: -# plugin names: F -# constants: I -# smtp commands, answers: B, B<250 Queued!> -# -# Notes: -# * due to restrictions of some POD parsers, no C<<$object->method()>> -# are allowed, use C<$object-Emethod()> -# - -=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: - - 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 -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. (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) - -=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 module if you know the size of the string (for more -info about netstrings see L). - -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:) - -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 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