Skip to content

Commit

Permalink
Merge branch 'issue_383_collectedsenders_receivers' (Fixes #383)
Browse files Browse the repository at this point in the history
  • Loading branch information
mstilkerich committed Jun 20, 2022
2 parents 83a9e39 + 40d3ca9 commit f71e708
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 11 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Changelog for RCMCardDAV

## Version 4.3.0 (to 4.3.0)
## Version 4.x.x (to 4.3.0)

- MySQL/PostgreSQL: Increase maximum length limit for addressbook name (Fixes #382)
- Fix: log messages could go to the wrong logger (carddav\_http.log) for a small part of the init code
- Support setting roundcube's collected senders/recipients to addressbooks from preset (Fixes #383)

## Version 4.3.0 (to 4.2.2)

Expand Down
110 changes: 102 additions & 8 deletions carddav.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
/**
* @psalm-type PasswordStoreScheme = 'plain' | 'base64' | 'des_key' | 'encrypted'
* @psalm-type ConfigurablePresetAttribute = 'name'|'url'|'username'|'password'|'active'|'refresh_time'
* @psalm-type SpecialAbookType = 'collected_recipients'|'collected_senders'
* @psalm-type SpecialAbookMatch = array{preset: string, matchname?: string, matchurl?: string}
* @psalm-type Preset = array{
* name: string,
* url: string,
Expand Down Expand Up @@ -117,6 +119,9 @@ class carddav extends rcube_plugin
/** @var PasswordStoreScheme encryption scheme */
private $pwStoreScheme = 'encrypted';

/** @var array<SpecialAbookType,SpecialAbookMatch> Match settings for special addressbooks */
private $specialAbookMatchers = [];

/** @var bool Global preference "fixed" */
private $forbidCustomAddressbooks = false;

Expand Down Expand Up @@ -217,6 +222,10 @@ function (string $id): string {
$config->set('autocomplete_addressbooks', array_merge($sources, $carddav_sources));
$skin_path = $this->local_skin_path();
$this->include_stylesheet($skin_path . '/carddav.css');

foreach ($this->specialAbookMatchers as $type => $matchSettings) {
$this->setSpecialAddressbook($type, $matchSettings);
}
} catch (\Exception $e) {
$logger->error("Could not init rcmcarddav: " . $e->getMessage());
}
Expand Down Expand Up @@ -655,26 +664,32 @@ public function savePreferences(array $args): array
* PRIVATE FUNCTIONS
**************************************************************************************/

private static function replacePlaceholdersUsername(string $username): string
private static function replacePlaceholdersUsername(string $username, bool $quoteRegExp = false): string
{
$rcube = rcube::get_instance();
$rcusername = (string) $_SESSION['username'];

$username = strtr($username, [
$transTable = [
'%u' => $rcusername,
'%l' => $rcube->user->get_username('local'),
'%d' => $rcube->user->get_username('domain'),
// %V parses username for macosx, replaces periods and @ by _, work around bugs in contacts.app
'%V' => strtr($rcusername, "@.", "__")
]);
];

if ($quoteRegExp) {
$transTable = array_map('preg_quote', $transTable);
}

$username = strtr($username, $transTable);

return $username;
}

private static function replacePlaceholdersUrl(string $url): string
private static function replacePlaceholdersUrl(string $url, bool $quoteRegExp = false): string
{
// currently same as for username
return self::replacePlaceholdersUsername($url);
return self::replacePlaceholdersUsername($url, $quoteRegExp);
}

private static function replacePlaceholdersPassword(string $password): string
Expand Down Expand Up @@ -1277,10 +1292,10 @@ private function readAdminSettings(): void
$this->forbidCustomAddressbooks = ($prefs['_GLOBAL']['fixed'] ?? false) ? true : false;
$this->hidePreferences = ($prefs['_GLOBAL']['hide_preferences'] ?? false) ? true : false;

foreach (['loglevel' => $logger, 'loglevel_http' => $httpLogger] as $setting => $logger) {
foreach (['loglevel' => $logger, 'loglevel_http' => $httpLogger] as $setting => $loggerobj) {
if (isset($prefs['_GLOBAL'][$setting]) && is_string($prefs['_GLOBAL'][$setting])) {
if ($logger instanceof RoundcubeLogger) {
$logger->setLogLevel($prefs['_GLOBAL'][$setting]);
if ($loggerobj instanceof RoundcubeLogger) {
$loggerobj->setLogLevel($prefs['_GLOBAL'][$setting]);
}
}
}
Expand All @@ -1304,6 +1319,36 @@ private function readAdminSettings(): void

$this->addPreset($presetname, $preset);
}

// Extract filter for special addressbooks
foreach (['collected_recipients', 'collected_senders'] as $setting) {
if (isset($prefs['_GLOBAL'][$setting]) && is_array($prefs['_GLOBAL'][$setting])) {
$matchSettings = $prefs['_GLOBAL'][$setting];

if (
isset($matchSettings['preset'])
&& is_string($matchSettings['preset'])
&& key_exists($matchSettings['preset'], $this->presets)
) {
$presetname = $matchSettings['preset'];
$matchSettings2 = [ 'preset' => $presetname ];
foreach (['matchname', 'matchurl'] as $matchType) {
if (isset($matchSettings[$matchType]) && is_string($matchSettings[$matchType])) {
$matchexpr = $matchSettings[$matchType];
$matchSettings2[$matchType] = $matchexpr;
}
}

if ($this->presets[$presetname]['readonly'] ?? false) {
$logger->error("Cannot use addressbooks from read-only preset $presetname for $setting");
} else {
$this->specialAbookMatchers[$setting] = $matchSettings2;
}
} else {
$logger->error("Setting for $setting must include a valid preset attribute");
}
}
}
}

/**
Expand Down Expand Up @@ -1363,6 +1408,55 @@ private function addPreset(string $presetname, array $preset): void
}
}

/**
* Sets one of the special addressbooks supported by roundcube to one of the carddav addressbooks.
*
* These special addressbooks as of roundcube 1.5 are collected recipients and collected senders. The admin can
* configure a match expression for the name or the URL of the addressbook, that is looked for in a specific preset.
*
* @param SpecialAbookType $type
* @param SpecialAbookMatch $matchSettings
*/
private function setSpecialAddressbook(string $type, array $matchSettings): void
{
$infra = Config::inst();
$logger = $infra->logger();
$presetname = $matchSettings['preset'];

$matches = [];
foreach ($this->getAddressbooks(true, true) as $abookrow) {
// check all addressbooks for that preset
if ($abookrow['presetname'] === $presetname) {
// All specified matchers must match
// If no matcher is set, any addressbook of the preset is considered a match
$isMatch = true;

foreach (['matchname', 'matchurl'] as $matchType) {
$matchexpr = $matchSettings[$matchType] ?? 0;
if (is_string($matchexpr)) {
$matchexpr = self::replacePlaceholdersUrl($matchexpr, true);
if (!preg_match($matchexpr, (string) $abookrow[substr($matchType, 5)])) {
$isMatch = false;
}
}
}

if ($isMatch) {
$matches[] = $abookrow['id'];
}
}
}

$numMatches = count($matches);
if ($numMatches != 1) {
$logger->error("Cannot set special addressbook $type, there are $numMatches candidates (need: 1)");
return;
}

$config = rcube::get_instance()->config;
$config->set($type, "carddav_" . $matches[0]);
}

// password helpers
private function getDesKey(): string
{
Expand Down
22 changes: 22 additions & 0 deletions config.inc.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@
$prefs['_GLOBAL']['loglevel'] = \Psr\Log\LogLevel::WARNING;
$prefs['_GLOBAL']['loglevel_http'] = \Psr\Log\LogLevel::ERROR;

// Select addressbook from preset to use as Roundcube's collected recipients or collected/trusted senders addressbook,
// corresponding to the roundcube options of the same name available since roundcube 1.5.
// Note that only writeable addressbooks can be used for this. If you do not want to use these options, simply do not
// define them
// If no/several addressbooks match, the roundcube setting will not be set and stay with whatever is configured in roundcube
//$prefs['_GLOBAL']['collected_recipients'] = [
// // Key of the preset, i.e. whatever is used for <Presetname> in the template below
// 'preset' => '<Presetname>',
// // The placeholders that can be used in the url attribute can also be used inside these regular rexpressions
// // If both matchname and matchurl are given, both need to match for the addressbook to be used
// 'matchname' => '/collected recipients/i',
// 'matchurl' => '#http://carddav.example.com/abooks/%u/CollectedRecipients#',
//];
//$prefs['_GLOBAL']['collected_senders'] = [
// // Key of the preset, i.e. whatever is used for <Presetname> in the template below
// 'preset' => '<Presetname>',
// // The placeholders that can be used in the url attribute can also be used inside these regular rexpressions
// // If both matchname and matchurl are given, both need to match for the addressbook to be used
// 'matchname' => '/collected recipients/i',
// 'matchurl' => '#http://carddav.example.com/abooks/%u/CollectedRecipients#',
//];

//// ** ADDRESSBOOK PRESETS

// Each addressbook preset takes the following form:
Expand Down
50 changes: 48 additions & 2 deletions doc/ADMIN-SETTINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ $prefs['Personal'] = [
'refresh_time' => '02:00:00',

'fixed' => ['username'],
'hide' => false,
'hide' => false,
];

// Preset 2: Corporate
Expand All @@ -229,7 +229,7 @@ $prefs['Work'] = [
'url' => 'corp.example.com',

'fixed' => ['name', 'username', 'password'],
'hide' => true,
'hide' => true,
];
```

Expand All @@ -248,4 +248,50 @@ Preconfigured addressbooks are processed when the user logs into roundcube.
- If the user has addressbooks created from a preset that no longer exists (identified by the Presetname), the
addressbooks are deleted from the database.

### Using presets for the roundcube trusted senders/collected recipients addressbooks

Roundcube 1.5 and newer has two special internal addressbooks to automatically collect all addresses the user previously
sent mail to (roundcube config option: `collected_recipients`) and to collect addresses of trusted senders (roundcube
config option: `collected_senders`).

It is possible for a user to manually select CardDAV addressbooks for these two special purpose addressbooks using
the roundcube settings interface. When using preconfigured CardDAV addressbooks, the admin may want to also set these
special addressbooks by configuration, which is possible using the following configuration options:

```php
$prefs['_GLOBAL']['collected_recipients'] = [
// Key of the preset
'preset' => '<Presetname>',
// The placeholders that can be used in the url preset attribute can also be used inside these regular rexpressions
'matchname' => '/collected recipients/i',
'matchurl' => '#http://carddav.example.com/abooks/%u/CollectedRecipients#',
];
$prefs['_GLOBAL']['collected_senders'] = [
// Configuration analog to collected recipients
];
```

Each of the above global RCMCardDAV settings will cause the roundcube setting of the same name to be overridden in case
a matching preset addressbook is found. The match works by specifying the key of a preset and further match settings to
filter the matching addressbook in case multiple addressbooks are discovered for the preset. The preset must not be
read-only as roundcube requires both special addressbooks to be writeable. For presets with several addressbooks, the
wanted addressbook can be identified by regular expression matches on the addressbook name and/or URL. The
%-placeholders the are possible in a preset URL also can be used inside these regular expressions.

In case the preset only contains one addressbook, the match settings can be omitted. The match settings must result in
exactly one addressbook. If no or multiple addressbooks match, the roundcube setting is not touched by RCMCardDAV.

Because RCMCardDAV overrides the setting configured in roundcube, including a possible setting by the user, the
possibilty to configure these addressbooks by the user should be disabled if the admin uses this mechanism. Otherwise
the user might be confused as settings made by the user in the roundcube settings will stay without effect.
Configuration of these addressbooks by the user can be disabled using the following configuration options in the
roundcube (not RCMCardDAV) configuration:

```php
$config['dont_override'] = ['collected_recipients', 'collected_senders'];
```

When using the trusted senders addressbook, please also configure the roundcube options `show_images` and `mdn_requests`
to define for what purpose the trusted senders are used.

<!-- vim: set ts=4 sw=4 expandtab fenc=utf8 ff=unix tw=120: -->

0 comments on commit f71e708

Please sign in to comment.