diff --git a/plugins/nextcloud/NextcloudAddressBook.php b/plugins/nextcloud/NextcloudAddressBook.php new file mode 100644 index 0000000000..e418c6dfe6 --- /dev/null +++ b/plugins/nextcloud/NextcloudAddressBook.php @@ -0,0 +1,207 @@ +defaultUri = $defaultUri; + $this->defaultName = $defaultName; + $this->defaultDescription = $defaultDescription; + $this->ignoreSystemAddressBook = $ignoreSystemAddressBook; + + $this->GetSavedAddressBookKey(); + } + + private function getContactsManager() + { + if ($this->contactsManager == null) { + $this->contactsManager = \OC::$server->getContactsManager(); + } + + return $this->contactsManager; + } + + private function GetSavedAddressBookKey() : string + { + if ($this->ignoreSystemAddressBook) { + foreach ($this->getContactsManager()->getUserAddressBooks() as $addressBook) { + if ($addressBook->isSystemAddressBook()) { + $this->getContactsManager()->unregisterAddressBook($addressBook); + } + } + } + + $uid = \OC::$server->getUserSession()->getUser()->getUID(); + $cardDavBackend = \OC::$server->get(\OCA\DAV\CardDAV\CardDavBackend::class); + $principalUri = 'principals/users/' . $uid; + $uri = $this->GetSavedUri(); + $addressBookId = $cardDavBackend->getAddressBooksByUri($principalUri, $uri); + + if ($addressBookId === null) { + return $cardDavBackend->createAddressBook($principalUri, $uri, array_filter([ + '{DAV:}displayname' => $this->defaultName, + '{urn:ietf:params:xml:ns:carddav}addressbook-description' => $this->defaultDescription, + ])); + } + + return $addressBookId['id']; + } + + public function IsSupported() : bool + { + // Maybe just return true, contacts app is just a frontend + // return \OC::$server->getAppManager()->isEnabledForUser('contacts'); + return true; + } + + public function SetEmail(string $sEmail) : bool + { + return true; + } + + public function Sync() : bool + { + return false; + } + + public function Export(string $sType = 'vcf') : bool + { + return false; + } + + public function ContactSave(Contact $oContact) : bool + { + return false; + } + + public function DeleteContacts(array $aContactIds) : bool + { + return false; + } + + public function DeleteAllContacts(string $sEmail) : bool + { + return false; + } + + public function GetContacts(int $iOffset = 0, int $iLimit = 20, string $sSearch = '', int &$iResultCount = 0) : array + { + return []; + } + + public function GetContactByEmail(string $sEmail) : ?Contact + { + return null; + } + + public function GetContactByID($mID, bool $bIsStrID = false) : ?Contact + { + return null; + } + + public function GetSuggestions(string $sSearch, int $iLimit = 20) : array + { + return []; + } + + private function GetEmailObjects(array $aEmails) : array + { + $aEmailsObjects = \array_map(function ($mItem) { + $oResult = null; + try { + $oResult = \MailSo\Mime\Email::Parse(\trim($mItem)); + } catch (\Throwable $oException) { + unset($oException); + } + return $oResult; + }, $aEmails); + + $aEmailsObjects = \array_filter($aEmailsObjects, function ($oItem) { + return !!$oItem; + }); + return $aEmailsObjects; + } + + /** + * Add/increment email address usage + * Handy for "most used" sorting suggestions in PdoAddressBook + */ + public function IncFrec(array $aEmails, bool $bCreateAuto = true) : bool + { + if ($bCreateAuto) { + $aEmailsObjects = $this->GetEmailObjects($aEmails); + + if (!count($aEmailsObjects)) { + return false; + } + + foreach ($aEmailsObjects as $oEmail) { + $this->createOrUpdateContact($oEmail); + } + + return true; + } + + return false; + } + + private function createOrUpdateContact($oEmail) + { + if ('' === \trim($oEmail->GetEmail())) { + return; + } + $sEmail = \trim($oEmail->GetEmail(true)); + $existingResults = $this->getContactsManager()->search($sEmail, ['EMAIL'], ['strict_search' => true]); + + if (!empty($existingResults)) { + return; + } + + $properties = [ + 'EMAIL' => $sEmail, + 'FN' => $sEmail + ]; + + if ('' !== \trim($oEmail->GetDisplayName())) { + $properties['FN'] = $oEmail->GetDisplayName(); + } + $this->getContactsManager()->createOrUpdate($properties, $this->GetSavedAddressBookKey()); + } + + public function Test() : string + { + return ''; + } + + private function Account() : \RainLoop\Model\Account + { + return \RainLoop\Api::Actions()->getAccountFromToken(); + } + + private function SettingsProvider() : \RainLoop\Providers\Settings + { + return \RainLoop\Api::Actions()->SettingsProvider(true); + } + + private function Settings() : \RainLoop\Settings + { + return $this->SettingsProvider()->Load($this->Account()); + } + + private function GetSavedUri() : string + { + return $this->Settings()->GetConf(self::SETTINGS_KEY, $this->defaultUri); + } +} diff --git a/plugins/nextcloud/README.md b/plugins/nextcloud/README.md new file mode 100644 index 0000000000..972e1cdc8e --- /dev/null +++ b/plugins/nextcloud/README.md @@ -0,0 +1,14 @@ +# SnappyMail plugin for nextcloud + +## Nextcloud Addressbook for recipients + + This plugin can let user to choose which nextcloud addressbook to use save recipients. This is opt-in feature (enabled by admin). After admin enable this, user will find a dropdown in his/her SnappyMail's `Contacts` section, containing all his/her addressbook. It works better with [Nextcloud Contacts App](https://github.com/nextcloud/contacts/) + +### Admin settings + +- `enableNcAddressbook` : Enable User to choose Nextcloud addressbook for recipients. Default value: `false` +- `disableSnappymailContactsUI` : Disable SnappyMail internal addressbook. This is recomended if nextcloud addressbook is being used. Default value: `false` +- `defaultNCAddressbookUri` : Default nextcloud addressbook URI for recipients. Default value: `webmail` +- `defaultNCAddressbookName` : Default nextcloud addressbook Name for recipients. Default value: `WebMail` +- `defaultNCAddressbookDescription` : Default nextcloud addressbook description for recipients. Default value: `Recipients from snappymail` + diff --git a/plugins/nextcloud/index.php b/plugins/nextcloud/index.php index 469ecf1b8a..5786043452 100644 --- a/plugins/nextcloud/index.php +++ b/plugins/nextcloud/index.php @@ -10,6 +10,26 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin DESCRIPTION = 'Integrate with Nextcloud v20+', REQUIRED = '2.36.2'; + private const IGNORE_SYSTEM_ADDRESSBOOK_KEY = 'ignoreSystemAddressbook'; + private const IGNORE_SYSTEM_ADDRESSBOOK_DEFAULT_VALUE = true; + + private const ENABLE_NC_ADDRESSBOOK_KEY = 'enableNcAddressbook'; + private const ENABLE_NC_ADDRESSBOOK_DEFAULT_VALUE = false; + + private const DISABLE_INHOUSE_ADDRESSBOOK_KEY = 'disableSnappymailContactsUI'; + private const DISABLE_INHOUSE_ADDRESSBOOK_DEFAULT_VALUE = false; + + private const DEFAULT_ADDRESSBOOK_URI_KEY = 'defaultNCAddressbookUri'; + private const DEFAULT_ADDRESSBOOK_URI = 'webmail'; + + private const DEFAULT_ADDRESSBOOK_NAME_KEY = 'defaultNCAddressbookName'; + private const DEFAULT_ADDRESSBOOK_NAME = 'WebMail'; + + private const DEFAULT_ADDRESSBOOK_DESCRIPTION_KEY = 'defaultNCAddressbookDescription'; + private const DEFAULT_ADDRESSBOOK_DESCRIPTION = 'Recipients from snappymail'; + + private const ADDRESSBOOK_SETTINGS_KEY = 'nextcloudAddressBookUri'; + public function Init() : void { if (static::IsIntegrated()) { @@ -41,6 +61,16 @@ public function Init() : void $this->addHook('imap.before-login', 'beforeLogin'); $this->addHook('smtp.before-login', 'beforeLogin'); $this->addHook('sieve.before-login', 'beforeLogin'); + + if ($this->Config()->Get('plugin', self::ENABLE_NC_ADDRESSBOOK_KEY, self::ENABLE_NC_ADDRESSBOOK_DEFAULT_VALUE)) { + $this->addJs('js/nextcloudAddressbook.js'); + $this->addJsonHook('NextcloudGetAddressBooks', 'GetAddressBooks'); + $this->addJsonHook('NextcloudUpdateAddressBook', 'UpdateAddressBook'); + } + + if ($this->Config()->Get('plugin', self::DISABLE_INHOUSE_ADDRESSBOOK_KEY, self::DISABLE_INHOUSE_ADDRESSBOOK_DEFAULT_VALUE)) { + $this->addJs('js/hideInhouseAddressbook.js'); + } } else { \SnappyMail\Log::debug('Nextcloud', 'NOT integrated'); // \OC::$server->getConfig()->getAppValue('snappymail', 'snappymail-no-embed'); @@ -48,6 +78,50 @@ public function Init() : void } } + public function GetAddressBooks() + { + $addressBooks = []; + + $contactsManager = \OC::$server->getContactsManager(); + + $defaultUri = $this->Config()->Get('plugin', self::DEFAULT_ADDRESSBOOK_URI_KEY, self::DEFAULT_ADDRESSBOOK_URI); + $selectedUri = $this->UserSettings()->GetConf(self::ADDRESSBOOK_SETTINGS_KEY, $defaultUri); + $ignoreSystemAddressbook = $this->Config()->Get('plugin', self::IGNORE_SYSTEM_ADDRESSBOOK_KEY, self::IGNORE_SYSTEM_ADDRESSBOOK_DEFAULT_VALUE); + + foreach ($contactsManager->getUserAddressBooks() as $addressBook) { + if ($ignoreSystemAddressbook && $addressBook->isSystemAddressBook()) { + $contactsManager->unregisterAddressBook($addressBook); + continue; + } + + $book = new AddressBook(); + $book->uri = $addressBook->getUri(); + $book->name = $addressBook->getDisplayName(); + if (strcmp($selectedUri, $book->uri) === 0) { + $book->selected = true; + } + + $addressBooks[] = $book; + } + + + return $this->jsonResponse(__FUNCTION__, array( + 'addressbooks' => json_encode($addressBooks) + )); + } + + public function UpdateAddressBook() : array + { + $uri = $this->jsonParam('uri'); + $oSettings = $this->UserSettings(); + if (\is_string($uri)) { + $oSettings->SetConf(self::ADDRESSBOOK_SETTINGS_KEY, $uri); + $this->SettingsProvider()->Save($this->Account(), $oSettings); + } + return $this->jsonResponse(__FUNCTION__, true); + } + + public function ContentSecurityPolicy(\SnappyMail\HTTP\CSP $CSP) { if (\method_exists($CSP, 'add')) { @@ -368,7 +442,26 @@ public function MainFabrica(string $sName, &$mResult) } include_once __DIR__ . '/NextcloudContactsSuggestions.php'; $mResult[] = new NextcloudContactsSuggestions( - $this->Config()->Get('plugin', 'ignoreSystemAddressbook', true) + $this->Config()->Get('plugin', self::IGNORE_SYSTEM_ADDRESSBOOK_KEY, self::IGNORE_SYSTEM_ADDRESSBOOK_DEFAULT_VALUE) + ); + } + + if ('address-book' === $sName && $this->Config()->Get('plugin', self::ENABLE_NC_ADDRESSBOOK_KEY, self::ENABLE_NC_ADDRESSBOOK_DEFAULT_VALUE)) { + if (!\is_array($mResult)) { + $mResult = array(); + } + include_once __DIR__ . '/NextcloudAddressBook.php'; + + $ignoreSystemAddressbook = $this->Config()->Get('plugin', self::IGNORE_SYSTEM_ADDRESSBOOK_KEY, self::IGNORE_SYSTEM_ADDRESSBOOK_DEFAULT_VALUE); + $defaultName = $this->Config()->Get('plugin', self::DEFAULT_ADDRESSBOOK_NAME_KEY, self::DEFAULT_ADDRESSBOOK_NAME); + $defaultDescription = $this->Config()->Get('plugin', self::DEFAULT_ADDRESSBOOK_DESCRIPTION_KEY, self::DEFAULT_ADDRESSBOOK_DESCRIPTION); + $defaultUri = $this->Config()->Get('plugin', self::DEFAULT_ADDRESSBOOK_URI_KEY, self::DEFAULT_ADDRESSBOOK_URI); + + $mResult = new NextcloudAddressBook( + $defaultUri, + $defaultName, + $defaultDescription, + $ignoreSystemAddressbook ); } /* @@ -386,9 +479,9 @@ protected function configMapping() : array \RainLoop\Plugins\Property::NewInstance('suggestions')->SetLabel('Suggestions') ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) ->SetDefaultValue(true), - \RainLoop\Plugins\Property::NewInstance('ignoreSystemAddressbook')->SetLabel('Ignore system addressbook') + \RainLoop\Plugins\Property::NewInstance(self::IGNORE_SYSTEM_ADDRESSBOOK_KEY)->SetLabel('Ignore system addressbook') ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) - ->SetDefaultValue(true), + ->SetDefaultValue(self::IGNORE_SYSTEM_ADDRESSBOOK_DEFAULT_VALUE), /* \RainLoop\Plugins\Property::NewInstance('storage')->SetLabel('Use Nextcloud user ID in config storage path') ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) @@ -396,7 +489,27 @@ protected function configMapping() : array */ \RainLoop\Plugins\Property::NewInstance('calendar')->SetLabel('Enable "Put ICS in calendar"') ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) - ->SetDefaultValue(false) + ->SetDefaultValue(false), + + \RainLoop\Plugins\Property::NewInstance(self::ENABLE_NC_ADDRESSBOOK_KEY)->SetLabel('Enable User to choose Nextcloud addressbook for recipients') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(self::ENABLE_NC_ADDRESSBOOK_DEFAULT_VALUE), + + \RainLoop\Plugins\Property::NewInstance(self::DEFAULT_ADDRESSBOOK_URI_KEY)->SetLabel('Default nextcloud addressbook URI for recipients') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetDefaultValue(self::DEFAULT_ADDRESSBOOK_URI), + + \RainLoop\Plugins\Property::NewInstance(self::DEFAULT_ADDRESSBOOK_NAME_KEY)->SetLabel('Default nextcloud addressbook Name for recipients') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetDefaultValue(self::DEFAULT_ADDRESSBOOK_NAME), + + \RainLoop\Plugins\Property::NewInstance(self::DEFAULT_ADDRESSBOOK_DESCRIPTION_KEY)->SetLabel('Default nextcloud addressbook description for recipients') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetDefaultValue(self::DEFAULT_ADDRESSBOOK_DESCRIPTION), + + \RainLoop\Plugins\Property::NewInstance(self::DISABLE_INHOUSE_ADDRESSBOOK_KEY)->SetLabel('Disable SnappyMail internal addressbook. This is recomended if nextcloud addressbook is being used.') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(self::DISABLE_INHOUSE_ADDRESSBOOK_DEFAULT_VALUE) ); } @@ -428,4 +541,26 @@ private static function SmartFileExists(string $sFilePath, $oFiles) : string } return $sFilePath; } + + private function Account() : \RainLoop\Model\Account + { + return \RainLoop\Api::Actions()->getAccountFromToken(); + } + + private function SettingsProvider() : \RainLoop\Providers\Settings + { + return \RainLoop\Api::Actions()->SettingsProvider(true); + } + + private function UserSettings() : \RainLoop\Settings + { + return $this->SettingsProvider()->Load($this->Account()); + } +} + +class AddressBook +{ + public string $uri; + public string $name; + public bool $selected = false; } diff --git a/plugins/nextcloud/js/hideInhouseAddressbook.js b/plugins/nextcloud/js/hideInhouseAddressbook.js new file mode 100644 index 0000000000..37feb7dc9d --- /dev/null +++ b/plugins/nextcloud/js/hideInhouseAddressbook.js @@ -0,0 +1,49 @@ +(rl => { + if (rl) { + addEventListener('rl-view-model', e => { + if ('MailFolderList' === e.detail.viewModelTemplateID) { + const container = e.detail.viewModelDom.querySelector('.buttonContacts'); + if (container) { + container.remove(); + } + } + }); + + + addEventListener('rl-view-model', e => { + if ('SystemDropDown' === e.detail.viewModelTemplateID) { + const container = e.detail.viewModelDom.querySelector('.dropdown-menu'); + if (!container) { + return; + } + + for (i = 0; i < container.children.length; i++) { + const element = container.children[i]; + const attr = element.getAttribute("data-bind"); + if (attr && attr.includes("visible: allowContacts")) { + element.remove(); + break; + } + } + } + }); + + addEventListener('rl-view-model', e => { + if ('PopupsCompose' === e.detail.viewModelTemplateID) { + const container = e.detail.viewModelDom.querySelector('.pull-right'); + if(!container) { + return; + } + + for (i = 0; i < container.children.length; i++) { + const element = container.children[i]; + const attr = element.getAttribute("data-bind"); + if (attr && attr.includes("visible: allowContacts")) { + element.remove(); + break; + } + } + } + }); + } +})(window.rl); diff --git a/plugins/nextcloud/js/nextcloudAddressbook.js b/plugins/nextcloud/js/nextcloudAddressbook.js new file mode 100644 index 0000000000..0c3684ce0b --- /dev/null +++ b/plugins/nextcloud/js/nextcloudAddressbook.js @@ -0,0 +1,42 @@ +(rl => { + if (rl) { + addEventListener('rl-view-model', e => { + if ('SettingsContacts' === e.detail.viewModelTemplateID) { + const container = e.detail.viewModelDom.querySelector('.form-horizontal'); + if (!container) { + return; + } + + rl.pluginRemoteRequest((iError, oData) => { + if (iError) { + return; + } + + const mainDivElement = Element.fromHTML('
' + + '' + + '
'); + + const selectElement = Element.fromHTML(''); + + const addressbooks = JSON.parse(oData.Result.addressbooks); + addressbooks.forEach(addressbook => { + if (addressbook.selected) { + selectElement.append(Element.fromHTML('')); + } else { + selectElement.append(Element.fromHTML('')); + } + }); + + selectElement.onchange = function() { + rl.pluginRemoteRequest(() => { }, 'NextcloudUpdateAddressBook', { + uri: selectElement.value + }); + } + + mainDivElement.append(selectElement); + container.append(mainDivElement); + }, "NextcloudGetAddressBooks"); + } + }); + } +})(window.rl); diff --git a/plugins/nextcloud/langs/de.json b/plugins/nextcloud/langs/de.json index 5f43f1357f..35c92421db 100644 --- a/plugins/nextcloud/langs/de.json +++ b/plugins/nextcloud/langs/de.json @@ -9,6 +9,7 @@ "SELECT_CALENDAR": "Kalender auswählen", "FILE_ATTACH": "anfügen", "FILE_INTERNAL": "intern", - "FILE_PUBLIC": "öffentlich" + "FILE_PUBLIC": "öffentlich", + "ADDRESS_BOOK": "Adressbuch" } } diff --git a/plugins/nextcloud/langs/en.json b/plugins/nextcloud/langs/en.json index 2c19d11c23..b4fb55a971 100644 --- a/plugins/nextcloud/langs/en.json +++ b/plugins/nextcloud/langs/en.json @@ -9,6 +9,7 @@ "SELECT_CALENDAR": "Select calendar", "FILE_ATTACH": "attach", "FILE_INTERNAL": "internal", - "FILE_PUBLIC": "public" + "FILE_PUBLIC": "public", + "ADDRESS_BOOK": "Address Book" } } diff --git a/plugins/nextcloud/langs/es.json b/plugins/nextcloud/langs/es.json new file mode 100644 index 0000000000..652f678912 --- /dev/null +++ b/plugins/nextcloud/langs/es.json @@ -0,0 +1,5 @@ +{ + "NEXTCLOUD": { + "ADDRESS_BOOK": "Libreta de direcciones" + } +} diff --git a/plugins/nextcloud/langs/fr.json b/plugins/nextcloud/langs/fr.json new file mode 100644 index 0000000000..6396152e67 --- /dev/null +++ b/plugins/nextcloud/langs/fr.json @@ -0,0 +1,5 @@ +{ + "NEXTCLOUD": { + "ADDRESS_BOOK": "Carnet d'adresses" + } +} diff --git a/plugins/nextcloud/langs/it.json b/plugins/nextcloud/langs/it.json new file mode 100644 index 0000000000..7d3f183a1a --- /dev/null +++ b/plugins/nextcloud/langs/it.json @@ -0,0 +1,5 @@ +{ + "NEXTCLOUD": { + "ADDRESS_BOOK": "Rubrica" + } +}