MailPlugin.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace OC\Collaboration\Collaborators;
  7. use OC\KnownUser\KnownUserService;
  8. use OCP\Collaboration\Collaborators\ISearchPlugin;
  9. use OCP\Collaboration\Collaborators\ISearchResult;
  10. use OCP\Collaboration\Collaborators\SearchResultType;
  11. use OCP\Contacts\IManager;
  12. use OCP\Federation\ICloudId;
  13. use OCP\Federation\ICloudIdManager;
  14. use OCP\IConfig;
  15. use OCP\IGroupManager;
  16. use OCP\IUser;
  17. use OCP\IUserSession;
  18. use OCP\Mail\IMailer;
  19. use OCP\Share\IShare;
  20. class MailPlugin implements ISearchPlugin {
  21. protected bool $shareWithGroupOnly;
  22. protected bool $shareeEnumeration;
  23. protected bool $shareeEnumerationInGroupOnly;
  24. protected bool $shareeEnumerationPhone;
  25. protected bool $shareeEnumerationFullMatch;
  26. protected bool $shareeEnumerationFullMatchEmail;
  27. public function __construct(
  28. private IManager $contactsManager,
  29. private ICloudIdManager $cloudIdManager,
  30. private IConfig $config,
  31. private IGroupManager $groupManager,
  32. private KnownUserService $knownUserService,
  33. private IUserSession $userSession,
  34. private IMailer $mailer,
  35. private mixed $shareWithGroupOnlyExcludeGroupsList = [],
  36. ) {
  37. $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
  38. $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
  39. $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
  40. $this->shareeEnumerationPhone = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
  41. $this->shareeEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
  42. $this->shareeEnumerationFullMatchEmail = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes';
  43. if ($this->shareWithGroupOnly) {
  44. $this->shareWithGroupOnlyExcludeGroupsList = json_decode($this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', ''), true) ?? [];
  45. }
  46. }
  47. /**
  48. * {@inheritdoc}
  49. */
  50. public function search($search, $limit, $offset, ISearchResult $searchResult): bool {
  51. if ($this->shareeEnumerationFullMatch && !$this->shareeEnumerationFullMatchEmail) {
  52. return false;
  53. }
  54. // Extract the email address from "Foo Bar <foo.bar@example.tld>" and then search with "foo.bar@example.tld" instead
  55. $result = preg_match('/<([^@]+@.+)>$/', $search, $matches);
  56. if ($result && filter_var($matches[1], FILTER_VALIDATE_EMAIL)) {
  57. return $this->search($matches[1], $limit, $offset, $searchResult);
  58. }
  59. $currentUserId = $this->userSession->getUser()->getUID();
  60. $result = $userResults = ['wide' => [], 'exact' => []];
  61. $userType = new SearchResultType('users');
  62. $emailType = new SearchResultType('emails');
  63. // Search in contacts
  64. $addressBookContacts = $this->contactsManager->search(
  65. $search,
  66. ['EMAIL', 'FN'],
  67. [
  68. 'limit' => $limit,
  69. 'offset' => $offset,
  70. 'enumeration' => $this->shareeEnumeration,
  71. 'fullmatch' => $this->shareeEnumerationFullMatch,
  72. ]
  73. );
  74. $lowerSearch = strtolower($search);
  75. foreach ($addressBookContacts as $contact) {
  76. if (isset($contact['EMAIL'])) {
  77. $emailAddresses = $contact['EMAIL'];
  78. if (\is_string($emailAddresses)) {
  79. $emailAddresses = [$emailAddresses];
  80. }
  81. foreach ($emailAddresses as $type => $emailAddress) {
  82. $displayName = $emailAddress;
  83. $emailAddressType = null;
  84. if (\is_array($emailAddress)) {
  85. $emailAddressData = $emailAddress;
  86. $emailAddress = $emailAddressData['value'];
  87. $emailAddressType = $emailAddressData['type'];
  88. }
  89. if (isset($contact['FN'])) {
  90. $displayName = $contact['FN'] . ' (' . $emailAddress . ')';
  91. }
  92. $exactEmailMatch = strtolower($emailAddress) === $lowerSearch;
  93. if (isset($contact['isLocalSystemBook'])) {
  94. if ($this->shareWithGroupOnly) {
  95. /*
  96. * Check if the user may share with the user associated with the e-mail of the just found contact
  97. */
  98. $userGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser());
  99. // ShareWithGroupOnly filtering
  100. $userGroups = array_diff($userGroups, $this->shareWithGroupOnlyExcludeGroupsList);
  101. $found = false;
  102. foreach ($userGroups as $userGroup) {
  103. if ($this->groupManager->isInGroup($contact['UID'], $userGroup)) {
  104. $found = true;
  105. break;
  106. }
  107. }
  108. if (!$found) {
  109. continue;
  110. }
  111. }
  112. if ($exactEmailMatch && $this->shareeEnumerationFullMatch) {
  113. try {
  114. $cloud = $this->cloudIdManager->resolveCloudId($contact['CLOUD'][0] ?? '');
  115. } catch (\InvalidArgumentException $e) {
  116. continue;
  117. }
  118. if (!$this->isCurrentUser($cloud) && !$searchResult->hasResult($userType, $cloud->getUser())) {
  119. $singleResult = [[
  120. 'label' => $displayName,
  121. 'uuid' => $contact['UID'] ?? $emailAddress,
  122. 'name' => $contact['FN'] ?? $displayName,
  123. 'value' => [
  124. 'shareType' => IShare::TYPE_USER,
  125. 'shareWith' => $cloud->getUser(),
  126. ],
  127. 'shareWithDisplayNameUnique' => !empty($emailAddress) ? $emailAddress : $cloud->getUser()
  128. ]];
  129. $searchResult->addResultSet($userType, [], $singleResult);
  130. $searchResult->markExactIdMatch($emailType);
  131. }
  132. return false;
  133. }
  134. if ($this->shareeEnumeration) {
  135. try {
  136. $cloud = $this->cloudIdManager->resolveCloudId($contact['CLOUD'][0] ?? '');
  137. } catch (\InvalidArgumentException $e) {
  138. continue;
  139. }
  140. $addToWide = !($this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone);
  141. if (!$addToWide && $this->shareeEnumerationPhone && $this->knownUserService->isKnownToUser($currentUserId, $contact['UID'])) {
  142. $addToWide = true;
  143. }
  144. if (!$addToWide && $this->shareeEnumerationInGroupOnly) {
  145. $addToWide = false;
  146. $userGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser());
  147. foreach ($userGroups as $userGroup) {
  148. if ($this->groupManager->isInGroup($contact['UID'], $userGroup)) {
  149. $addToWide = true;
  150. break;
  151. }
  152. }
  153. }
  154. if ($addToWide && !$this->isCurrentUser($cloud) && !$searchResult->hasResult($userType, $cloud->getUser())) {
  155. $userResults['wide'][] = [
  156. 'label' => $displayName,
  157. 'uuid' => $contact['UID'] ?? $emailAddress,
  158. 'name' => $contact['FN'] ?? $displayName,
  159. 'value' => [
  160. 'shareType' => IShare::TYPE_USER,
  161. 'shareWith' => $cloud->getUser(),
  162. ],
  163. 'shareWithDisplayNameUnique' => !empty($emailAddress) ? $emailAddress : $cloud->getUser()
  164. ];
  165. continue;
  166. }
  167. }
  168. continue;
  169. }
  170. if ($exactEmailMatch
  171. || (isset($contact['FN']) && strtolower($contact['FN']) === $lowerSearch)) {
  172. if ($exactEmailMatch) {
  173. $searchResult->markExactIdMatch($emailType);
  174. }
  175. $result['exact'][] = [
  176. 'label' => $displayName,
  177. 'uuid' => $contact['UID'] ?? $emailAddress,
  178. 'name' => $contact['FN'] ?? $displayName,
  179. 'type' => $emailAddressType ?? '',
  180. 'value' => [
  181. 'shareType' => IShare::TYPE_EMAIL,
  182. 'shareWith' => $emailAddress,
  183. ],
  184. ];
  185. } else {
  186. $result['wide'][] = [
  187. 'label' => $displayName,
  188. 'uuid' => $contact['UID'] ?? $emailAddress,
  189. 'name' => $contact['FN'] ?? $displayName,
  190. 'type' => $emailAddressType ?? '',
  191. 'value' => [
  192. 'shareType' => IShare::TYPE_EMAIL,
  193. 'shareWith' => $emailAddress,
  194. ],
  195. ];
  196. }
  197. }
  198. }
  199. }
  200. $reachedEnd = true;
  201. if ($this->shareeEnumeration) {
  202. $reachedEnd = (count($result['wide']) < $offset + $limit) &&
  203. (count($userResults['wide']) < $offset + $limit);
  204. $result['wide'] = array_slice($result['wide'], $offset, $limit);
  205. $userResults['wide'] = array_slice($userResults['wide'], $offset, $limit);
  206. }
  207. if (!$searchResult->hasExactIdMatch($emailType) && $this->mailer->validateMailAddress($search)) {
  208. $result['exact'][] = [
  209. 'label' => $search,
  210. 'uuid' => $search,
  211. 'value' => [
  212. 'shareType' => IShare::TYPE_EMAIL,
  213. 'shareWith' => $search,
  214. ],
  215. ];
  216. }
  217. if (!empty($userResults['wide'])) {
  218. $searchResult->addResultSet($userType, $userResults['wide'], []);
  219. }
  220. $searchResult->addResultSet($emailType, $result['wide'], $result['exact']);
  221. return !$reachedEnd;
  222. }
  223. public function isCurrentUser(ICloudId $cloud): bool {
  224. $currentUser = $this->userSession->getUser();
  225. return $currentUser instanceof IUser && $currentUser->getUID() === $cloud->getUser();
  226. }
  227. }