Server Settings Backend plugin for SquirrelMail
===============================================
Ver 1.0, 2009/02/05


Copyright (c) 2008-2009 Paul Lesniewski <paul@squirrelmail.org>



Description
===========

This plugin provides a highly configurable way for other
plugins to retrieve and store settings outside of SquirrelMail.
Values stored in a SQL database, in LDAP, on a server accessed
by FTP, or on the local file system (usually accessed via a
set-uid wrapper that allows read and write access to files not
owned or normally accessible by the web server) are all supported.
Even the credentials used for accessing the settings (for example,
the FTP login username and password) can be recursively passed
back to this plugin so that they themselves can be looked up in
any one of the supported data stores.

This plugin serves as a library for other plugins (not
necessarily just the Server Settings plugin), thus it never
needs to be activated itself.  Merely download and unpack it
in the SquirrelMail plugins directory and no more.



License
=======

This plugin is released under the GNU General Public 
License (see the file COPYING for details).



Donations
=========

If you or your company make regular use of this software, please
consider supporting Open Source development by donating to the authors
or inquire about hiring them to consult on other projects.  Donation
links for the author(s) are as follows:

Paul Lesniewski: https://sourceforge.net/donate/index.php?user_id=508228



Requirements
============

  * SquirrelMail version 1.4.0+
  * When not using SquirrelMail 1.5.2+, Compatibility
    plugin, version 2.0.14+
  * Pear DB and/or MDB2 package, if using the SQL database backend
  * FTP support in PHP, if using the FTP backend



Troubleshooting
===============

  * Note that if any recursive configuration items (that you may
    use a separate lookup to retrieve if needed) are misconfigured
    (such as the DSN for a SQL database-backed setting), they may
    produce seemingly unrelated error messages, such as "lookup
    unknown".  Make sure to double-check all syntax when configuring
    this plugin.

  * If you are getting errors due to the lack of FTP support in
    your PHP build (which is required if you use any FTP-backed
    settings), you'll need to recompile PHP with the --enable-ftp
    option.  For more information, see:

       http://www.php.net/manual/ref.ftp.php

  * If you are getting errors due to the lack of a database
    abstraction layer, make sure you have either NOT configured
    one in your configuration or that if you have, you have
    not mistyped it.  Currently, only DB and MDB2 are suppored.
    See the SQL configuration section, DATABASE_ABSTRACTION_LAYER
    item.

  * If you are getting errors due to the lack of Pear DB or MDB2
    library on your system (one of which is required if you use
    any SQL database-backed settings), try typing "pear install DB"
    or "pear install MDB2" on your command line (only one is
    necessary depending on which abstraction layer you have
    chosen - if you have NOT chosen one specifically, the plugin
    defaults to using DB).  For more information, see:

       http://pear.php.net/manual/en/installation.php

  * If you are using MDB2 as your database interface API and 
    are getting an error such as "MDB2 Error: not found" (make
    sure to turn on debugging to see more error details), then
    make sure you have the correct driver installed for your
    database as well.  For example, when using MySQL, you'll
    need to do a "pear install MDB2#mysql" or
    "pear install MDB2_Driver_mysql".  If errors persist,
    (such as "pear/MDB2_Driver_XXX requires php extension XXX"),
    please consult the MDB2 FAQ:

       http://pear.php.net/manual/en/package.database.mdb2.faq.php

  * If changes to the configuration file don't seem to be having any
    effect, ensure that there are not two Server Settings Backend
    configuration files, one in the server_settings_backend directory
    and one in the main SquirrelMail config directory (named
    "config_server_settings_backend.php").  The one in the main
    SquirrelMail config directory will always override the one in the
    server_settings_backend directory.



Help Requests
=============

Before looking for help elsewhere, please try to help yourself:

  * Read the Troubleshooting section herein.

  * Look to see if others have already asked about the same issue.
    There are tips and links for the best places to do this in
    the SquirrelMail mailing list posting guidelines:
    http://squirrelmail.org/wiki/MailingListPostingGuidelines
    You should also try Google or some other search engine.

  * If you cannot find any information about your issue, please
    first mail your help request to the squirrelmail-plugins
    mailing list.  Information about it can be found here:
    http://lists.sourceforge.net/mailman/listinfo/squirrelmail-plugins
    You MUST read the mailing list posting guidelines (see above)
    and include as much information about your issue (and your
    system) as possible.  Including configtest output, any debug
    output, the plugin configuration settings you've made and
    anything else you can think of to make it easier to diagnose
    your problem will get you the most useful responses.  Inquiries
    that do not comply with the posting guidelines are liable to
    be ignored.

  * If you don't get any replies on the mailing list, you are
    welcome to send a help request to the authors' personal
    address(es), but please be patient with the mailing list.



Configuration
=============

Configuration for this plugin is found in whatever plugin uses
the libraries herein.  Thus, the following serves as a guide for
the parts of those plugins that concern themselves with how to
access settings stored outside of SquirrelMail.  Please refer to
the plugin documentation for any plugin that uses this one, as
the location and format of these configuration settings might be
slightly different therein.  The Server Settings plugin is one
example of a plugin that makes use of this one.

Typically, *each* setting that is found outside of SquirrelMail
needs an array of settings to help identify it and how it is
accessed outside of SquirrelMail.  Each setting has its own
configuration so that you can simultaneously access settings on
any number of different backend systems.  However, it is possible
that a plugin that makes use of this one will simplify the
configuration process by using the same storage backend settings
for all settings.  What configuration values are needed depends
upon the backend in which the setting is located.

SQL Backend
-----------

Here is an example of some of the most common (and required,
except the "USERNAME" element) configuration values when using
the SQL database backend:

array(
   'BACKEND'    => 'sql',
   'DSN'        => array('VALUE' => 'mysql://username:password@localhost/database'),
   'QUERY'      => array('VALUE' => "SELECT color FROM favorites WHERE username = '%1@%2'"),
   'SAVE_QUERY' => array('VALUE' => "UPDATE favorites SET color = '%3' WHERE username = '%1@%2'"),
   'USERNAME'   => array('STRIP_DOMAIN' => 1),
)

The "BACKEND" key is what dictates where the setting is located (in
a SQL database?  In LDAP?  Etc....) and what other configuration
items are needed.  If, however, you specify a key called "VALUE",
its corresponding value will be immediately used instead of making
any backend lookups - this allows you to hard-code a configuration
item.  Another key called "CUSTOM" allows you to specify an external
PHP function name that will be consulted for obtaining the desired
setting.  That function must be provided by you, and is expected
to return the setting value.  It will be given the entire contents
of the configuration array as its one and only argument, so it can
make use of any custom information you might add to it.  If "VALUE"
is specified, it always takes precendence.  If "CUSTOM" is
specified, it gets second priority before falling back on "BACKEND"
to make a standard lookup.

   'VALUE'  => 'xyz',
   'CUSTOM' => 'my_lookup_function',

The "DSN" is what indicates how to connect to the database.  For
information about the syntax of the DSN, please see:

   http://pear.php.net/manual/en/package.database.db.intro-dsn.php
   http://pear.php.net/manual/en/package.database.mdb2.intro-dsn.php

By default, the database abstraction layer used by this plugin is Pear
DB, however, if you'd like to use MDB2 instead, you can specify a
different "DATABASE_ABSTRACTION_LAYER"

   'DATABASE_ABSTRACTION_LAYER' => array('VALUE' => 'MDB2'),

The "QUERY" key contains the SQL command for how to retrieve the setting's
value.  The "SAVE_QUERY" contains the SQL command for how to update the
value in the database.  You should be SURE that any and all SQL commands
are enclosed in DOUBLE quotes, or lookups are liable to break or become
exposed to security exploits.  Before either of these commands is used,
the following substitutions will be made, if found within them:

   %1  The username of the user who is currently logged in
   %2  The domain of the user who is currently logged in

The "SAVE_QUERY" has one additional substitution that is made:

   %3  The new value being sent to the database

The "USERNAME" key is used to perform manipulations on the current user's
username before using it in the SQL commands.  It should conatin an array
of username manipulation configuration rules.  It is optional, and if not
specified, the username will be used as is.  Please refer to the
Credentials Manipulations section elsewhere for the available "USERNAME"
configuration items.

When working with settings that contain multiple values (say, for example,
a whitelist of email addresses), there are a few choices with how those
values will be stored in the database.  They can be compressed into a
single encoded string or stored in multiple database entries.  In order
to compress the values into one string, you may add the following to your
list of configuration items:

   'MULTIPLE' => ',',

This will create a comma-separated-values format from the values and
store them all in one database entry.  Instead of a comma, you may
use anything you like, but you must consider that if your actual values
contain the same character(s), your data will become corrupt.  Instead,
you can use PHP serialization (which will not suffer from such
corruption, but does require slighly more storage space in your database,
so please make sure your column size is big enough) by specifying:

   'MULTIPLE' => 'SERIALIZE',

If, on the other hand, you want to store your value in multiple
database rows (perhaps your whitelist table is designed to have
a two-column primary key of username and (single) whitelist
address), you can specify:

   'MULTIPLE' => 'MULTIPLE',

If your "QUERY" returns multiple rows but you have not specified
anything for the "MULTIPLE" configuraion, values will be compressed
using PHP serialization, but you should always use the "MULTIPLE"
configuration to avoid problems.

Note that when storing multiple rows, using the SQL UPDATE command
will not produce the desired results.  Instead, you'll want to use
the SQL INSERT command.  However, this leaves no way to remove old
values from the database when updating them.  In this case, you can
specify a special "PRE_SAVE_QUERY" SQL command that will be run once
before your data is saved (it will also be subject to the same "%1"
and "%2" substitutions as the normal "QUERY"):

   'PRE_SAVE_QUERY' => array('VALUE' => "DELETE FROM whitelist WHERE username = '%1@%2'"),
   'SAVE_QUERY'     => array('VALUE' => "INSERT INTO whitelist (username, sender) VALUES ('%1@%2', '%3')"),

Likewise, when SquirrelMail needs to test a setting if it matches a
certain value, the "QUERY" command will work perfectly fine for a
scalar setting, but not very efficiently for a setting that contains
multiple values.  In order to reduce the load on your database and
make such lookups generally much more efficient, you may optionally
specify as special "TEST_QUERY":

   'TEST_QUERY' => array('VALUE' => "SELECT sender FROM whitelist WHERE username = '%1@%2' AND sender = '%3'"),

The substitutions for "TEST_QUERY" are the same as for "SAVE_QUERY".
Note that you'll want to be careful to select the value of the target
row and column because the code test will still be done against the
test value ("%3") as if it were a scalar value.

The "PRE_SAVE_QUERY" can also be used in scalar contexts:  consider
a scenario where a string or integer setting may *not* already be in
the target database table for all users.  "SAVE_QUERY" would therefore
need to use the INSERT syntax, but subsequent save actions would
trigger errors (or corrupt the database) because the query should use
UPDATE instead.  Here, too, the solution is to specify a
"PRE_SAVE_QUERY" with the same DELETE syntax as shown above:

   'PRE_SAVE_QUERY' => array('VALUE' => "DELETE FROM preferences WHERE username = '%1@%2' AND preference_name = 'favorite_color'"),
   'SAVE_QUERY'     => array('VALUE' => "INSERT INTO preferences (username, preference_name, value) VALUES ('%1@%2', 'favorite_color', '%3')"),

Other solutions that could be used in the preceeding scenario are
a REPLACE query in the "SAVE_QUERY" (and no "PRE_SAVE_QUERY" needed),
an INSERT... ON DUPLICATE KEY UPDATE query in the "SAVE_QUERY" (and
again, no "PRE_SAVE_QUERY" needed) or a conditional insert action
such as INSERT... SELECT... FROM DUAL WHERE NOT EXISTS (SELECT...)
subquery in the "PRE_SAVE_QUERY" along with a standard UPDATE in the
"SAVE_QUERY" (some of these options may not be supported in the
database being used; check your documentation).  Some examples of
these can be found in the SQL example configuration file for the
Server Settings plugin.

When a value is being added to a multi-value setting (see the Usage
Guide below regarding the arguments (specifically, $add) to
put_server_setting()), it may in some cases be more efficient to use
a different kind of SQL command to just add that one value (and also
avoid executing the "PRE_SAVE_QUERY").  For any settings that have
the "MULTIPLE" item configured, they may specify an "ADD_QUERY" that
does exactly that.

   'ADD_QUERY' => array('VALUE' => "INSERT INTO whitelist (username, sender) VALUES ('%1@%2', '%3')"),

In this exmple, the command syntax is no different, but the command
is run only once for the new value and "PRE_SAVE_QUERY" will be
skipped, boosting performance just a touch.  The substitutions for
"ADD_QUERY" are the same as for "SAVE_QUERY".

Likewise, when removing a value from a multi-value setting (again,
see the Usage Guide below regarding the arguments (specifically,
$remove) to put_server_setting()), you may use a special
"REMOVE_QUERY" item that will be used to remove just the one value
in question and avoid running the "PRE_SAVE_QUERY".  The
substitutions for "REMOVE_QUERY" are the same as for "SAVE_QUERY".

   'REMOVE_QUERY' => array('VALUE' => "DELETE FROM whitelist WHERE username = '%1@%2' AND SENDER = '%3'"),

For values that are stored in a single database row (single value
settings or multiple value settings compressed into one (using
serialization, comma-separated-values format, etc.)), it is also
possible to delete the setting from the database altogether when
its value is empty.  To do this, the "REMOVE_QUERY" must be specified
along with "DELETE_WHEN_EMPTY".  Set "DELETE_WHEN_EMPTY" to 1 to
enable this behavior or 0 (zero) to leave the empty setting in the
database (which is the default behavior when this element is not
present).  Note that the "REMOVE_QUERY" here will not usually be
the same query as when it is being used with multiple-value settings
(see above).

   'DELETE_WHEN_EMPTY' => array('VALUE' => 1),
   'REMOVE_QUERY'      => array('VALUE' => "DELETE FROM whitelist WHERE username = '%1@%2'"),

You'll notice that some of the configuration settings are embedded in
sub-arrays with the key "VALUE".  These (specifically, any of "DSN",
"DATABASE_ABSTRACTION_LAYER", "QUERY", "SAVE_QUERY", "PRE_SAVE_QUERY",
"TEST_QUERY", "ADD_QUERY", "REMOVE_QUERY" or "DELETE_WHEN_EMPTY") can
all themselves be the subjects of separate backend lookups.  If, for
example, you need to look up the DSN for a certain setting, you can do
something like this:

array(
   'BACKEND'    => 'sql',
   'DSN'        => array(
                      'BACKEND'  => 'sql',
                      'DSN'      => array('VALUE' => 'mysql://username:password@localhost/database'),
                      'QUERY'    => array('VALUE' => "SELECT dsn FROM client_databases WHERE domain = '%2'"),
                   ),
   'QUERY'      => array('VALUE' => "SELECT color FROM favorites WHERE username = '%1@%2'"),
   'SAVE_QUERY' => array('VALUE' => "UPDATE favorites SET color = '%3' WHERE username = '%1@%2'"),
   'USERNAME'   => array('STRIP_DOMAIN' => 1),
)

LDAP Backend
------------

Not yet implemented.

FTP Backend
-----------

Here is an example of some of the most common configuration
values when using the FTP backend:

   'BACKEND'              => 'ftp',
   'HOST'                 => array('VALUE' => 'localhost'),
   'MODE'                 => array('VALUE' => 'BINARY'),
   'DIRECTORY'            => array('VALUE' => '.spamassassin'),
   FILE'                 => array('VALUE' => 'user_prefs'),
   'PARSE_PATTERN'        => array('VALUE' => "/^required_score\s+(.*)$/m"),
   'PATTERN_GROUP_NUMBER' => array('VALUE' => 1),
   'NEW_SETTING_TEMPLATE' => array('VALUE' => "required_score  $1\n"),
   'DELETE_WHEN_EMPTY'    => array('VALUE' => 1),
   'MAX_SEQUENTIAL_EMPTY_LINES' => 3,
   'TREAT_AS_EMPTY_WHEN_NOT_FOUND' => array('VALUE' => 1),

The minimal required elements are: "BACKEND", "HOST", "FILE",
"PARSE_PATTERN" and "NEW_SETTING_TEMPLATE".

The "BACKEND" key is what dictates where the setting is located (in
a SQL database?  In LDAP?  Etc....) and what other configuration
items are needed.  If, however, you specify a key called "VALUE",
its corresponding value will be immediately used instead of making
any backend lookups - this allows you to hard-code a configuration
item.  Another key called "CUSTOM" allows you to specify an external
PHP function name that will be consulted for obtaining the desired
setting.  That function must be provided by you, and is expected
to return the setting value.  It will be given the entire contents
of the configuration array as its one and only argument, so it can
make use of any custom information you might add to it.  If "VALUE"
is specified, it always takes precendence.  If "CUSTOM" is
specified, it gets second priority before falling back on "BACKEND"
to make a standard lookup.

   'VALUE'  => 'xyz',
   'CUSTOM' => 'my_lookup_function',

The "HOST" is what indicates the address of the FTP server.  "MODE"
indicates the transfer mode of put and get operations and must be
either "BINARY" or "ASCII" (if not specified, "BINARY" is assumed).
"DIRECTORY" specifies the directory where the target file is found,
and if not given, the target file is assumed to be in whatever
directory the FTP server places you in upon login.  "FILE" must
contain the name of the target file.

Some other settings (not shown in the example above) that may be
specified are: "PORT", which indicates the port to be used when
connecting to the FTP server (when not given, 21 is assumed).
"SSL" can be used to indicate that the FTP connection should be
done over SSL (note that this is not the same as the SFTP
protocol; to use SSL-FTP, you need to have SSL support built into
your PHP build).  To enable, it should be 1; when not given,
0 (zero) is assumed, which disables SSL-FTP).  "PASSIVE" can be
used to indicate the status of passive mode FTP connections (1 = on,
0 (zero)= off; default is off).

   'PORT'    => array('VALUE' => 990),
   'SSL'     => array('VALUE' => 1),
   'PASSIVE' => array('VALUE' => 1),

For information about enabling SSL in PHP, see:

   http://php.net/manual/ref.openssl.php

"TREAT_AS_EMPTY_WHEN_NOT_FOUND" specifies whether or not, when the
file is not found, the setting value should be assumed to be
empty/null (if this is the desired behavior, set to 1).  If not
specified, 0 (zero) is assumed, and an error will be caused whenever
a setting's target file is not found on the FTP server.

"DELETE_WHEN_EMPTY" indicates that when nothing but whitespace
is left in the target file (perhaps due to the user deactivating
some setting(s) or clearing a text field), it should be deleted.
Set to 1 to enable this behavior or 0 (zero) to leave the empty
file on the server (which is the default behavior when this
element is not present).

"NEW_SETTING_TEMPLATE" is used to add a setting to its target
file when it is not yet present therein.  The value given here
is, after making the substitutions noted below, added to the
end of the target file.  Before it is used, the following
substitutions will be made, if found:

   %1  The new setting value that is being added
   %u  The current user's parsed (per the USERNAME rules
       explained elsewhere in this document) user name
   %d  The current user's parsed (per the DOMAIN rules
       explained elsewhere in this document) domain name

When "NEW_SETTING_TEMPLATE" is used to add setting(s) to the
target file, they are added to the end of the file by default.
If they should be added to the top of the file instead, use the
"ADD_TO_TOP" entry:

   'ADD_TO_TOP' => array('VALUE' => 1),

Because FTP is a file-level protocol, once the file is downloaded,
the setting value needs to be picked out of that file.  To provide
maximum flexibility, "PARSE_PATTERN" can be given as any regular
expression needed to do just that.  For example, to pull a setting
that occurs on just one line that starts with the setting name
and then some whitespace before the setting value, this would do:

   'PARSE_PATTERN' => array('VALUE' => "/^required_score\s+(.*)$/m"),

Note that the example above works as expected even when the
setting may appear multiple times in the file on more than one
line (as long as MULTIPLE is set correctly).  When the entire
file is needed, this pattern is appropriate:

   'PARSE_PATTERN'  => array('VALUE' => "/^(.*)$/s"),

Note that the setting's value is indicated in these examples by
being enclosed in parenthesis.  In more complex patterns, more
that one set of parenthesis may be required, so in order to
indicate which set of parenthesis contains the needed value, use
"PATTERN_GROUP_NUMBER", which should contain the sequential
number of the desired parenthesis set, counting opening parenthesis
from left to right.  In the two examples above, we want the first
parenthesis pair:

   'PATTERN_GROUP_NUMBER' => array('VALUE' => 1),

If "PATTERN_GROUP_NUMBER" is not given, 0 (zero) is assumed,
which takes the *entire* pattern match.  For more information
about how to design your own custom regular expressions, see:

   http://php.net/manual/reference.pcre.pattern.syntax.php
   http://php.net/manual/reference.pcre.pattern.modifiers.php

One important thing to keep in mind about the "PARSE_PATTERN"
is that it is both used as explained above to pick out a
setting's value from a file, as well as to identify what should
be removed from a file when the setting is being removed (perhaps
the user deselected the setting) or when it is being removed
and then re-added to the file later (which usually only applies
to when multiple-value settings are being reshuffled).  In some
cases, it is desirable to keep some small amount of the setting
in the file after it has been removed (maybe set it to some
default value or keep a placeholder).  You may accomplish
this using the "DELETE_KEEP_PATTERN" element, which is used as
the replacement portion of the regular expression search and
replace operation that is performed on the file.  You can use
any features of normal regular expression replacement strings,
such as back references ($1, $2, etc.), as explained in the PHP
regular expression manual.  Additionally, if "%u" or "%d" are
found in "DELETE_KEEP_PATTERN", they are replaced with the pre-
parsed username and domain before the regular expression is
executed.  If "DELETE_KEEP_PATTERN" is not given, it is not used
and the entire "PARSE_PATTERN" match is removed from the file
when appropriate.

   'DELETE_KEEP_PATTERN' => array('VALUE' => '$3 = none'),

When removing (and possibly replacing) lines from the target file,
the "PARSE_PATTERN" might leave behind extra unwanted blank lines,
gradually growing the file out of control with empty space.  To
avoid this, you may set "MAX_SEQUENTIAL_EMPTY_LINES" to the maximum
allowable number of blank lines in the file.  Any more will be
removed.  Note that this setting will affect the entire file, and
not just the lines that were removed for this setting.  If set to
0 (zero), no blank line replacement will be attempted.  Also note
that this entry is NOT a lookup item like the other entries
described here - it is specified only as a simple integer value.

   'MAX_SEQUENTIAL_EMPTY_LINES' => 1,

In some limited circumstances, you may want to remove a given value
from a file on the backend when a save action is occuring.  This
usually only applies when a secondary save action is in use (see
the Advanced Usage section elsewhere and refer to the example FTP
configuration file for a working example).  In this case, you can
specify a "DELETE_INSTEAD_OF_ADD" entry whose value should then
be 1 (if not specified, 0 (zero) is assumed).  This will force the
setting value being saved to instead be removed from the file (if
found therein).

   'DELETE_INSTEAD_OF_ADD' => array('VALUE' => 1),

It is also possible to use "PARSE_PATTERN" with a corresponding
replacement pattern that can be used when updating single-value
settings.  Instead of just replacing the pattern identified by
"PARSE_PATTERN" with the new value, you can specify a regular
expression replacement pattern in "REPLACEMENT_PATTERN" (where
you can also use back references and any other features of normal
regular expression replacements).  Before it is used, there are
three character sequence substitutions that are made (see below)
if needed.  Typically, this is an advanced usage and is not
recommended unless you know what you are doing.

   'REPLACEMENT_PATTERN' => array('VALUE' => "$1\nSetting: %1\n$2"),

Before "REPLACEMENT_PATTERN" is used, the following substitutions
will be made, if found:

   %1  The new setting value that is being updated
   %u  The current user's parsed (per the USERNAME rules
       explained elsewhere in this document) user name
   %d  The current user's parsed (per the DOMAIN rules
       explained elsewhere in this document) domain name

It is also possible to bypass the use of "PARSE_PATTERN" entirely
when updating settings in the target file.  You can indicate that
"PARSE_PATTERN" is only for use to retrieve a setting by specifying
a separate search AND replacement pattern that will be used when
updating the setting.  You can use full regular expression syntax
in them both, including back references, etc.  For example:

   'UPDATE_SEARCH_PATTERN'  => array('VALUE' => '/^(?(?=Subject:[^\n]*)([^\n]*\n*)|())(.*)/is'),
   'UPDATE_REPLACE_PATTERN' => array('VALUE' => "\${1}%1"),

The same substitutions (for "%1", "%u" and "%d") that are done for
"REPLACEMENT_PATTERN" (see immediately above) are also made on both
"UPDATE_SEARCH_PATTERN" and "UPDATE_REPLACE_PATTERN" before they
are executed.

Note that "REPLACEMENT_PATTERN", "UPDATE_SEARCH_PATTERN", and
"UPDATE_REPLACE_PATTERN" are all applied only when their pattern
matches something in the target file.  If not, standard
functionality is reverted to, which usually means that the
"NEW_SETTING_TEMPLATE" will be used to add a new value to the
file.

The "USERNAME" key is used to perform manipulations on the current
user's username before using it to log into the FTP server.  It
should conatin an array of username manipulation configuration rules.
It is optional, and if not specified, the username will be used as is.
Please refer to the Credentials Manipulations section elsewhere for
the available "USERNAME" configuration items.  If you use a single
username for all FTP logins (not recommended), you can specify a
hard-coded value here as well.  Here are two examples:

   'USERNAME' => array('STRIP_DOMAIN' => 1),
   'USERNAME' => array('VALUE' => 'ftp_user'),

The "PASSWORD" key is used to perform manipulations on the current
user's password before using it to log into the FTP server.  It
should conatin an array of password manipulation configuration rules.
It is optional, and if not specified, the password will be used as is.
Please refer to the Credentials Manipulations section elsewhere for
the available "PASSWORD" configuration items.  If you use a single
password for all FTP logins (not recommended), you can specify a
hard-coded value here as well.  Here are two examples:

   'PASSWORD' => array('VALUE' => 'master_ftp_password'),
   'PASSWORD' => array('REGULAR_EXPRESSION_PATTERN' => '/[-+* =&]/'
                       'REGULAR_EXPRESSION_REPLACEMENT' => '_'),

When working with settings that contain multiple values (say, for
example, a whitelist of email addresses), there are a few choices
with how those values will be stored in the file.  They can be
compressed into a single encoded string or stored in multiple
entries.  In order to compress the values into one string, you may
add the following to your list of configuration items:

   'MULTIPLE' => ',',

This will create a comma-separated-values format from the values
and store them all in one entry.  Instead of a comma, you may use
anything you like, but you must consider that if your actual values
contain the same character(s), your data will become corrupt.
Instead, you can use PHP serialization (which will not suffer from
such corruption, but does require slighly more storage space) by
specifying:

   'MULTIPLE' => 'SERIALIZE',

If, on the other hand, you want to store your value in multiple
entries in the same file, you can specify:

   'MULTIPLE' => 'MULTIPLE',

If your "PARSE_PATTERN" returns multiple setting values but you
have not specified anything for the "MULTIPLE" configuraion, values
will be compressed using PHP serialization, but you should always
use the "MULTIPLE" configuration to avoid problems.

Local File Backend
------------------

Not yet implemented.

Credentials Manipulations
-------------------------

Many of the backends used in this plugin require some kind of
username, domain and/or password information.  You'll need to
refer to the documentation for the backend in question as to
if and how these modified credentials are used.

The "USERNAME" key is used to perform manipulations on the current
user's username.  It should conatin an array of username manipulation
configuration rules.  It is optional, and if not specified, the
username will be used as is.  The available username manipulations
are any one of the following:

   STRIP_DOMAIN  This removes any domain information from the
                 username as well as the email delimiter, if
                 found.  So, if your usernames are of the format
                 "user@example.org", the resulting username for
                 use as the "%1" substitution in the SQL commands
                 will be just "user".  If the domain is not found
                 the username will be unchanged, so it does not
                 hurt to leave this turned on always.  The domain
                 is identified by the presence of the email
                 delimiter, so you'll want to make sure it is
                 also set appropriately for your system.  This
                 should be enabled by setting it to something
                 non-zero:
                    'STRIP_DOMAIN' => 1

   ADD_DOMAIN    This adds an email delimiter and the current domain
                 to the username if they are not already found in
                 the username.  The domain being added may either
                 be the original SquirrelMail domain or the one
                 that results after applying the "DOMAIN"
                 maniuplations specified herein.  To specify one
                 or the other:
                    'ADD_DOMAIN' => 'ORIGINAL'
                    'ADD_DOMAIN' => 'MODIFIED'

   DELIMITER     This specifies the email delimiter used for the
                 other rules herein, and if not given, defaults to
                 "@".  You could change it to "&" as such:
                    'DELIMITER' => '&'

   REGULAR_EXPRESSION_PATTERN
   REGULAR_EXPRESSION_REPLACEMENT
                 These two items are used to pass the username
                 through a regular expression pattern replacement.
                 This allows any manner of complex replacements
                 to be made as necessary.  Both must be specified
                 to make use of this functionality.
                    'REGULAR_EXPRESSION_PATTERN' => '/example.com$/'
                    'REGULAR_EXPRESSION_REPLACEMENT' => 'example.org'

   VALUE         When this is specified, the username will be used
                 as given here - it will be the same for ALL USERS.
                 This might be helpful when you have just one
                 username and password for making FTP lookups.
                    'VALUE' => 'master.user.name'

   CUSTOM        When this is specified, the username will be looked
                 up by calling a custom function that you provide.
                 That function must then return the username.  It is
                 given the entire contents of the "USERNAME" array
                 of configuration items as a parameter, so your
                 function can make use of any custom information you
                 add to it.
                    'CUSTOM' => 'my_username_lookup'

   BACKEND       When this is specified, the username will be looked
                 up by making a separate backend inquiry.  In this
                 case, this "USERNAME" sub-array will need to
                 contain all the same backend configuration items
                 for the desired lookup.  This might be useful when,
                 for example, you need to consult an LDAP data store
                 or SQL database to determine what the FTP login
                 information is for the current user when making a
                 FTP lookup.  Note that the use of the recursive
                 "USERNAME" item below is perfectly acceptable.
                    'BACKEND'  => 'sql'
                    'DSN'      => array('VALUE' => 'mysql://username:password@localhost/database'),
                    'QUERY'    => array('VALUE' => "SELECT username FROM ftp_accounts WHERE username = '%1@%2'"),
                    'USERNAME' => array('STRIP_DOMAIN' => 1),

The "DOMAIN" key is used to perform manipulations on the current
user's domain.  It should conatin an array of domain manipulation
configuration rules.  It is optional, and if not specified, the
domain will be used as is.  The available domain manipulations
are any one of the following:

   REGULAR_EXPRESSION_PATTERN
   REGULAR_EXPRESSION_REPLACEMENT
                 These two items are used to pass the domain
                 through a regular expression pattern replacement.
                 This allows any manner of complex replacements
                 to be made as necessary.  Both must be specified
                 to make use of this functionality.
                    'REGULAR_EXPRESSION_PATTERN' => '/^example.com$/'
                    'REGULAR_EXPRESSION_REPLACEMENT' => 'example.org'

   VALUE         When this is specified, the domain will be used
                 as given here - it will be the same for ALL USERS.
                    'VALUE' => 'master.domain.name'

   CUSTOM        When this is specified, the domain will be looked
                 up by calling a custom function that you provide.
                 That function must then return the domain.  It is
                 given the entire contents of the "DOMAIN" array
                 of configuration items as a parameter, so your
                 function can make use of any custom information you
                 add to it.
                    'CUSTOM' => 'my_domain_lookup'

   BACKEND       When this is specified, the domain will be looked
                 up by making a separate backend inquiry.  In this
                 case, this "DOMAIN" sub-array will need to
                 contain all the same backend configuration items
                 for the desired lookup.
                    'BACKEND'  => 'sql'
                    'DSN'      => array('VALUE' => 'mysql://username:password@localhost/database'),
                    'QUERY'    => array('VALUE' => "SELECT domain FROM user_domains WHERE username = '%1@%2'"),
                    'USERNAME' => array('STRIP_DOMAIN' => 1),

The "PASSWORD" key is used to perform manipulations on the current
user's password.  It should conatin an array of password manipulation
configuration rules.  It is optional, and if not specified, the
password will be used as is.  The available password manipulations
are any one of the following:

   REGULAR_EXPRESSION_PATTERN
   REGULAR_EXPRESSION_REPLACEMENT
                 These two items are used to pass the password
                 through a regular expression pattern replacement.
                 This allows any manner of complex replacements
                 to be made as necessary.  Both must be specified
                 to make use of this functionality.
                    'REGULAR_EXPRESSION_PATTERN' => '/aeiou/'
                    'REGULAR_EXPRESSION_REPLACEMENT' => ''

   VALUE         When this is specified, the password will be used
                 as given here - it will be the same for ALL USERS.
                 This might be helpful when you have just one
                 username and password for making FTP lookups.
                    'VALUE' => 'master.password'

   CUSTOM        When this is specified, the password will be looked
                 up by calling a custom function that you provide.
                 That function must then return the password.  It is
                 given the entire contents of the "PASSWORD" array
                 of configuration items as a parameter, so your
                 function can make use of any custom information you
                 add to it.
                    'CUSTOM' => 'my_password_lookup'

   BACKEND       When this is specified, the password will be looked
                 up by making a separate backend inquiry.  In this
                 case, this "PASSWORD" sub-array will need to
                 contain all the same backend configuration items
                 for the desired lookup.  This might be useful when,
                 for example, you need to consult an LDAP data store
                 or SQL database to determine what the FTP login
                 information is for the current user when making a
                 FTP lookup.
                    'BACKEND'  => 'sql'
                    'DSN'      => array('VALUE' => 'mysql://username:password@localhost/database'),
                    'QUERY'    => array('VALUE' => "SELECT password FROM ftp_accounts WHERE username = '%1@%2'"),
                    'USERNAME' => array('STRIP_DOMAIN' => 1),

Advanced Usage
--------------

Also allowed is an advanced usage where more than one set of
configuration values is given.  This allows more than one save
action to be performed for any one setting.  In this case, the
lookup will always use the FIRST set of configuration values,
but each configuration set will be used once when saving the
setting's value.  Note that each set may have different BACKEND
types.

array(
   array(
      'BACKEND'              => 'ftp',
      'HOST'                 => array('VALUE' => 'localhost'),
      'MODE'                 => array('VALUE' => 'BINARY'),
      'FILE'                 => array('VALUE' => 'autoreply_message_body'),
      'PARSE_PATTERN'        => array('VALUE' => "/^(.*)$/s"),
      'NEW_SETTING_TEMPLATE' => array('VALUE' => '%1'),
      'TREAT_AS_EMPTY_WHEN_NOT_FOUND' => array('VALUE' => 1),
      'DELETE_WHEN_EMPTY'    => array('VALUE' => 1),
   ),
   array(
      'BACKEND'    => 'sql',
      'DSN'        => array('VALUE' => 'mysql://username:password@localhost/autoresponder_database'),
      'SAVE_QUERY' => array('VALUE' => "UPDATE autoreply_messages SET message_body = '%3' WHERE username = '%1@%2'"),
      'USERNAME'   => array('STRIP_DOMAIN' => 1),
   ),
)



Usage (Plugin Authors' Guide)
=============================

Retrieve/Store Settings
-----------------------

The points of entry to this plugin related to storing and
retrieving setting values are few and simple - one for
retrieving a setting value, one for testing a setting's value,
and one for saving a value.  Note that all of them have a
parameter called $quiet.  If it is TRUE, any errors are
silently ignored and NULL is returned.  Typically, you'll
want to leave $quiet set to FALSE, since any errors
encountered are usually configuration errors that you will
want to see and debug.

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/functions.php');
   $setting = retrieve_server_setting($storage_info, $quiet);

This will return the desired setting value (whose name is specified
in the $storage_info variable, which is explained in the Configuration
section).  If the setting value consists of multiple values, it will
be returned in an array, unless there is a configuration problem in
the $storage_info variable.

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/functions.php');
   $result = test_server_setting($test_value, $storage_info, $quiet);

This function will test if the setting identified in the $storage_info
variable (see the Configuration section) contains the given test value.
If the setting is comprised of multiple values, it is considered a
match if one of the values therein matches the test value, whereas if
the setting is scalar, its value must match the test value exactly to
produce an affirmative return value.  The returned value is boolean,
indicating if the test matched or not.  

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/functions.php');
   $result = put_server_setting($new_value, $storage_info, $add, $remove, $quiet);

This will return TRUE when the setting was saved normally.  Here, too,
the $storage_info variable contains all the information needed to
identify the setting name and where and how it is accessed.  See the
Configuration section for more details.  When $add is TRUE, it
indicates that a single value is to be added on to a multi-value
setting.  This allows the caller to push an additional value into the
setting without needing to worry about retrieving and re-saving all
the other values it contains.  Similarly, if $remove is TRUE, this
indicates that the given value is to be removed from a multi-value
setting.  This function will return TRUE when the setting was saved
normally.

File Management API
-------------------

There are an additional set of API entry points for managing files in
a server backend.  The backend type must be one of the file-based
protocols (so, for example, SQL and LDAP backend types won't work and
will produce errors if used with the following functions).  They all
share a $backend_info parameter, which is mostly identical to the
$storage_info parameter to the functions explained above (and defined
in detail in the Configuration section), except that they typically 
only use a subset of the entries therein, ignoring things such as
entries that define how to parse a setting out of a file or how
multiple values are represented.  The $quiet parameter also works
the same as explained above.  For examples of the use of these
functions, see the File Manager plugin, version 3.0.
   
   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/file_functions.php');
   $listing = retrieve_directory_listing($directory, $backend_info, $quiet);

This retrieves a listing of the files and directories within a
designated directory.  It returns an array containing sub-arrays
which each represent a file or directory in the listing.  These
sub-arrays will contain possibly several different key-value pairs,
each for a different file attribute, but should always at least
contain a "name" key whose value is the file or directory name.
Other possible attributes are: "type" ("file", "directory", "link",
"pipe", "socket"), "owner", "group", "size", "permissions", "date"
(date last modified), "link_saki" (link destination),
"numeric_permissions".  The $directory parameter should be the full
path to the desired directory (keep in mind that this may be
relative to where the user is rooted, especially when using an FTP
backend).

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/file_functions.php');
   $result = path_exists($path, $return_info, $backend_info, $quiet);

This will return TRUE when the specified file or directory exists on
the server, or FALSE if not.  The $path parameter should be the full
path to the desired file or directory (keep in mind that this may be
relative to where the user is rooted, especially when using an FTP
backend).  When $return_info is TRUE, an array of file (or directory)
information will be returned instead (one of the attribute arrays
identical to what is explained above).

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/file_functions.php');
   $result = create_directory($directory, $mode, $backend_info, $quiet);

This will return TRUE when the desired directory was created normally.
The $directory parameter should be the full path to the desired
directory (keep in mind that this may be relative to where the user is
rooted, especially when using an FTP backend).  The $mode parameter is
the octal permissions number to apply to the newly created directory,
which may be ignored depending on the backend (using 0755 is a common
default you can use unless more restricted access is desired).

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/file_functions.php');
   $result = delete_file_or_directory($path, $is_directory, $backend_info, $quiet);

This will return TRUE when the desired directory or file was deleted
normally.  The $path parameter should be the full path to the desired
file or directory (keep in mind that this may be relative to where the
user is rooted, especially when using an FTP backend).  The $is_directory
parameter should be TRUE if it is a directory being deleted and FALSE if
it is a file (some backends might ignore this and take the appropriate
action based on its own test of the path).  Remember that directories
must be empty before deleting them.  Note that if it is possible that
users will try to delete non-empty directories, it may be best to set
$quiet to TRUE and catch NULL return values, displaying a warning to
the user.

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/file_functions.php');
   $result = create_file($path, $contents, $mode, $allow_overwrite, $backend_info, $quiet);

This will return TRUE when the desired file was created normally.
The $path parameter should be the full path to the desired file
(keep in mind that this may be relative to where the user is rooted,
especially when using an FTP backend).  $contents should contain
anything that should be put in the new file, but need not contain
anything.  The $mode parameter is the octal permissions number to
apply to the newly created file, which may be ignored depending on
the backend (using 0644 is a common default you can use unless more
restricted access is desired).  Note that when $allow_overwrite is
TRUE, this function will replace any file that already exists; if
$allow_overwrite is FALSE and $path already exists, an error will
be tripped.

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/file_functions.php');
   $result = change_permissions($path, $mode, $backend_info, $quiet);

This will return TRUE when the desired file has had its permissions
changed to $mode, which is the octal permissions number to apply.
The $path parameter should be the full path to the desired file
(keep in mind that this may be relative to where the user is rooted,
especially when using an FTP backend).  Note that not all backends
support this functionality, so depending on the situation it may be
useful to specify $quiet as TRUE and deal with the return value
(TRUE or NULL/FALSE) as needed.

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/file_functions.php');
   $contents = get_file($path, $backend_info, $quiet);

This will return the contents of the desired file.  The $path
parameter should be the full path to the desired file (keep in
mind that this may be relative to where the user is rooted,
especially when using an FTP backend).

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/file_functions.php');
   $result = move_file_or_directory($source_path, $destination_path, $allow_overwrite, $backend_info, $quiet);

This will return TRUE when the file or directory has been renamed
or moved to the new location.  The $source_path parameter should
be the full path to the file or directory to be renamed/moved
(keep in mind that this may be relative to where the user is
rooted, especially when using an FTP backend).  Likewise, the
$destination_path is the target location, which should also be a
full path.  Note that when $allow_overwrite is TRUE, this function
will replace any file that already exists; if $allow_overwrite is
FALSE and $path already exists, an error will be tripped.

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/file_functions.php');
   $result = copy_file_or_directory($source_path, $destination_path, $is_directory, $recursive, $allow_overwrite, $backend_info, $quiet);

This will return TRUE when the file or directory has been copied
to the new location.  The $source_path parameter should be the
full path to the file or directory to be copied (keep in mind
that this may be relative to where the user is rooted, especially
when using an FTP backend).  Likewise, the $destination_path is
the target location, which should also be a full path.  The
$is_directory parameter should be TRUE if it is a directory
being copied and FALSE if it is a file (some backends might
ignore this and take the appropriate action based on its own
test of the path).  When $recursive is TRUE and a directory is
being copied, any subdirectories and their contents are
recursively copied as well; otherwise only the immediate
contents of the directory (first level) are copied - no
subdirectories will be included.  Links are not copied at all.
Note that when $allow_overwrite is TRUE, this function will
replace any file or directory that already exists at the
destination; if $allow_overwrite is FALSE and $destination_path
already exists, an error will be tripped.

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/file_functions.php');
   $file_path = create_archive($path, $use_path, $is_directory, $recursive, $backend_info, $quiet);

This will build an archive (zip-compressed) file containing the
file or directory pointed to by $path.  $path should be the full
path to the file or directory to be archived (keep in mind that
this may be relative to where the user is rooted, especially when
using an FTP backend).  The $use_path parameter (must be a boolean
value) indicates whether or not the full path to the item being
archived will be included in the archive or not.  The
$is_directory parameter should be TRUE if it is a directory being
archived and FALSE if it is a file (some backends might ignore this
and take the appropriate action based on its own test of the path).
When $recursive is TRUE and a directory is being archived, any
subdirectories and their contents are recursively included in the
archive; otherwise only the immediate contents of the directory
(first level) are archived - no subdirectories will be included.
The path to the archive file on the local file system (usually in
the SquirrelMail attachments directory) is returned under normal
conditions.

   $quiet = FALSE;
   include_once(SM_PATH . 'plugins/server_settings_backend/file_functions.php');
   $size = calculate_size($path, $is_directory, $recursive, $backend_info, $quiet);

This returns the approximate size of the file or directory pointed
to by $path.  $path should be the full path to the file or directory
whose size is to be determined (keep in mind that this may be
relative to where the user is rooted, especially when using an FTP
backend).  The $is_directory parameter should be TRUE if it is a
directory being calculated and FALSE if it is a file (some backends
might ignore this and take the appropriate action based on its own
test of the path).  When $recursive is TRUE and a directory is being
calculated, any subdirectories and their contents are recursively
included in the final size calculation; otherwise only the immediate
contents of the directory (first level) are counted - no
subdirectories will be included.



TODO
====

  * Implement unfinished backends: LDAP, LOCAL_FILE (suid *OR*
    apache write access).  In order to complete these, I need
    some motivation and reliable testers.

  * Implement some other backend(s)?  What one(s)?

  * Just a note that the following project might be useful or
    maybe just have some code worth borrowing if the FTP backend
    herein needs work: http://sourceforge.net/projects/kioobftp/



Change Log
==========

  v1.0  2009/02/05  Paul Lesniewski <paul@squirrelmail.org>
    * Initial release; only SQL and FTP backends have been
      implemented so far



