Repository: owncloud/ocis
Pull request #6119 (2023-04-24 09:06:09)

[docs-only] Add caching/storing info to services where missing
References: #6019 ([full-ci] Refactor stores)

**DO NOT MERGE** before the referenced PR got merged !

Adds/corrects missing info in README.md for services with respect to caching/storing.

@aduffeck @butonic fyi


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c316dd69ab6..68d2d29415f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,7 +15,6 @@ The following sections list the changes for unreleased.
 * Bugfix - Fix Search reindexing performance regression: [#6085](https://github.com/owncloud/ocis/pull/6085)
 * Bugfix - Fix Search tag indexing: [#5405](https://github.com/owncloud/ocis/pull/5405)
 * Bugfix - Fix user type config for user provider: [#6027](https://github.com/owncloud/ocis/pull/6027)
-* Bugfix - Fix userlog panic: [#6114](https://github.com/owncloud/ocis/pull/6114)
 * Bugfix - Add missing CORS config: [#5987](https://github.com/owncloud/ocis/pull/5987)
 * Bugfix - Populate expanded properties: [#5421](https://github.com/owncloud/ocis/pull/5421)
 * Bugfix - Add portrait thumbnail resolutions: [#5656](https://github.com/owncloud/ocis/pull/5656)
diff --git a/changelog/unreleased/fix-userlog-panic.md b/changelog/unreleased/fix-userlog-panic.md
deleted file mode 100644
index f8ea47b4d24..00000000000
--- a/changelog/unreleased/fix-userlog-panic.md
+++ /dev/null
@@ -1,5 +0,0 @@
-Bugfix: Fix userlog panic
-
-userlog services paniced because of `nil` ctx. That is fixed now
-
-https://github.com/owncloud/ocis/pull/6114
diff --git a/changelog/unreleased/users-preferred-language-email.md b/changelog/unreleased/users-preferred-language-email.md
deleted file mode 100644
index 9f2672de63b..00000000000
--- a/changelog/unreleased/users-preferred-language-email.md
+++ /dev/null
@@ -1,5 +0,0 @@
-Enhancement: Determine the users language to translate via Transifex
-
-Enhance userlog service with proper api and messages
-https://github.com/owncloud/ocis/pull/6089
-https://github.com/owncloud/ocis/issues/6087
diff --git a/docs/helpers/index.tmpl b/docs/helpers/index.tmpl
index 2c011dc652d..0ea33427be4 100644
--- a/docs/helpers/index.tmpl
+++ b/docs/helpers/index.tmpl
@@ -3,13 +3,11 @@ title: {{ .ServiceName }}
 date: {{ .CreationTime }}
 weight: 20
 geekdocRepo: https://github.com/owncloud/ocis
-geekdocEditPath: edit/master/services/{{ .service }}
-geekdocFilePath: README.md
+geekdocEditPath: edit/master/docs/services/{{ .service }}
+geekdocFilePath: _index.md
 geekdocCollapseSection: true
 ---
 
-<!-- Do not edit this file, it is autogenerated. Edit the service README.md instead -->
-
 ## Abstract
 
 {{ .Abstract }}
diff --git a/services/eventhistory/README.md b/services/eventhistory/README.md
index 31bdd610e9b..0eac0968ad8 100644
--- a/services/eventhistory/README.md
+++ b/services/eventhistory/README.md
@@ -1,6 +1,6 @@
 # Eventhistory
 
-The `eventhistory` consumes all events from the configured event system like NATS, stores them and allows other services to retrieve them via an eventid.
+The `eventhistory` consumes all events from the configured event system like NATS, stores them and allows other services to retrieve them via an event ID.
 
 ## Prerequisites
 
@@ -15,18 +15,18 @@ The `eventhistory` services consumes all events from the configured event system
 The `eventhistory` service stores each consumed event via the configured store in `EVENTHISTORY_STORE_TYPE`. Possible stores are:
   -   `memory`: Basic in-memory store and the default.
   -   `ocmem`: Advanced in-memory store allowing max size.
-  -   `redis`: Stores data in a configured redis cluster.
-  -   `redis-sentinel`: Stores data in a configured redis sentinel cluster.
+  -   `redis`: Stores data in a configured Redis cluster.
+  -   `redis-sentinel`: Stores data in a configured Redis Sentinel cluster.
   -   `etcd`: Stores data in a configured etcd cluster.
   -   `nats-js`: Stores data using key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
   -   `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
 
-1.  Note that in-memory stores are by nature not reboot persistent.
-2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store applies.
+1.  Note that in-memory stores are by nature not reboot-persistent.
+2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store apply.
 3.  Events stay in the store for 2 weeks by default. Use `EVENTHISTORY_RECORD_EXPIRY` to adjust this value.
 4.  The eventhistory service can be scaled if not using `in-memory` stores and the stores are configured identically over all instances.
 5.  When using `redis-sentinel`, the Redis master to use is configured via `EVENTHISTORY_STORE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
 
 ## Retrieving
 
-Other services can call the `eventhistory` service via a grpc call to retrieve events. The request must contain the eventid that should be retrieved.
+Other services can call the `eventhistory` service via a gRPC call to retrieve events. The request must contain the event ID that should be retrieved.
diff --git a/services/frontend/README.md b/services/frontend/README.md
index bb6383f9478..a7667d426cf 100644
--- a/services/frontend/README.md
+++ b/services/frontend/README.md
@@ -26,16 +26,32 @@ The ocs endpoint, by default `/ocs`, implements the ownCloud 10 Open Collaborati
 
 Aggregating share information is one of the most time consuming operations in OCIS. The service fetches a list of either received or created shares and has to stat every resource individually. While stats are fast, the default behavior scales linearly with the number of shares.
 
-To save network trips the sharing implementation can cache the stat requests with an in memory cache or in redis. It will shorten the response time by the network round-trip overhead at the cost of the API only eventually being updated.
+To save network trips the sharing implementation can cache the stat requests with an in memory cache or in Redis. It will shorten the response time by the network round-trip overhead at the cost of the API only eventually being updated.
 
 Setting `FRONTEND_OCS_RESOURCE_INFO_CACHE_TTL=60` would cache the stat info for 60 seconds. Increasing this value makes sense for large deployments with thousands of active users that keep the cache up to date. Low frequency usage scenarios should not expect a noticeable improvement.
 
 ## Scalability
 
-While the frontend service does not persist any data it does cache `Stat()` responses and user information. Therefore, multiple instances of this service can be spawned in a bigger deployment like when using container orchestration with Kubernetes, when configuring `FRONTEND_OCS_RESOURCE_INFO_CACHE_TYPE=redis` and the related config options.
+While the frontend service does not persist any data, it does cache information about files and filesystem (`Stat()`) responses and user information. Therefore, multiple instances of this service can be spawned in a bigger deployment like when using container orchestration with Kubernetes, when configuring `FRONTEND_OCS_RESOURCE_INFO_CACHE_STORE` and the related config options.
 
 ## Define Read-Only Attributes
 
 A lot of user management is made via the standardized libregraph API. Depending on how the system is configured, there might be some user attributes that an ocis instance admin can't change because of properties coming from an external LDAP server, or similar. This can be the case when the ocis admin is not the LDAP admin. To ease life for admins, there are hints as capabilites telling the frontend which attributes are read-only to enable a different optical representation like being grayed out. To configure these hints, use the environment variable `FRONTEND_READONLY_USER_ATTRIBUTES`, which takes a comma separated list of attributes, see the envvar for supported values.
 
 You can find more details regarding available attributes at the [libre-graph-api openapi-spec](https://github.com/owncloud/libre-graph-api/blob/main/api/openapi-spec/v1.0.yaml) and on [owncloud.dev](https://owncloud.dev/libre-graph-api/).
+
+## Caching
+
+The `frontend` service can use a configured store via `FRONTEND_OCS_RESOURCE_INFO_CACHE_STORE`. Possible stores are:
+  -   `memory`: Basic in-memory store and the default.
+  -   `ocmem`: Advanced in-memory store allowing max size.
+  -   `redis`: Stores data in a configured Redis cluster.
+  -   `redis-sentinel`: Stores data in a configured Redis Sentinel cluster.
+  -   `etcd`: Stores data in a configured etcd cluster.
+  -   `nats-js`: Stores data using key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
+  -   `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
+
+1.  Note that in-memory stores are by nature not reboot-persistent.
+2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store apply.
+3.  The frontend service can be scaled if not using `in-memory` stores and the stores are configured identically over all instances.
+4.  When using `redis-sentinel`, the Redis master to use is configured via `FRONTEND_OCS_RESOURCE_INFO_CACHE_STORE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
diff --git a/services/gateway/README.md b/services/gateway/README.md
index e0a197ab973..e718e9a4465 100644
--- a/services/gateway/README.md
+++ b/services/gateway/README.md
@@ -6,9 +6,14 @@ The gateway service is an ...
 
 The `gateway` service can use a configured store via `GATEWAY_CACHE_STORE`. Possible stores are:
   -   `memory`: Basic in-memory store and the default.
-  -   `redis`: Stores data in a configured redis cluster.
+  -   `ocmem`: Advanced in-memory store allowing max size.
+  -   `redis`: Stores data in a configured Redis cluster.
+  -   `redis-sentinel`: Stores data in a configured Redis Sentinel cluster.
   -   `etcd`: Stores data in a configured etcd cluster.
+  -   `nats-js`: Stores data using key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
+  -   `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
 
-1.  Note that in-memory stores are by nature not reboot persistent.
-2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store applies.
+1.  Note that in-memory stores are by nature not reboot-persistent.
+2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store apply.
 3.  The gateway service can be scaled if not using `in-memory` stores and the stores are configured identically over all instances.
+4.  When using `redis-sentinel`, the Redis master to use is configured via `GATEWAY_CACHE_STORE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
diff --git a/services/graph/README.md b/services/graph/README.md
index 84b3aab67d0..ad2b3d8b4ef 100644
--- a/services/graph/README.md
+++ b/services/graph/README.md
@@ -20,14 +20,14 @@ The following image gives an overview of the scenario when a client requests to
 The `graph` service can use a configured store via `GRAPH_STORE_TYPE`. Possible stores are:
   -   `memory`: Basic in-memory store and the default.
   -   `ocmem`: Advanced in-memory store allowing max size.
-  -   `redis`: Stores data in a configured redis cluster.
-  -   `redis-sentinel`: Stores data in a configured redis sentinel cluster.
+  -   `redis`: Stores data in a configured Redis cluster.
+  -   `redis-sentinel`: Stores data in a configured Redis Sentinel cluster.
   -   `etcd`: Stores data in a configured etcd cluster.
   -   `nats-js`: Stores data using key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
   -   `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
 
-1.  Note that in-memory stores are by nature not reboot persistent.
-2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store applies.
+1.  Note that in-memory stores are by nature not reboot-persistent.
+2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store apply.
 3.  The graph service can be scaled if not using `in-memory` stores and the stores are configured identically over all instances.
 4.  When using `redis-sentinel`, the Redis master to use is configured via `GRAPH_CACHE_STORE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
 
diff --git a/services/notifications/pkg/command/server.go b/services/notifications/pkg/command/server.go
index 2bacb06619d..d5eff832243 100644
--- a/services/notifications/pkg/command/server.go
+++ b/services/notifications/pkg/command/server.go
@@ -12,8 +12,6 @@ import (
 	"github.com/go-micro/plugins/v4/events/natsjs"
 	"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
 	"github.com/owncloud/ocis/v2/ocis-pkg/crypto"
-	"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
-	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	"github.com/owncloud/ocis/v2/services/notifications/pkg/channels"
 	"github.com/owncloud/ocis/v2/services/notifications/pkg/config"
 	"github.com/owncloud/ocis/v2/services/notifications/pkg/config/parser"
@@ -95,8 +93,8 @@ func Server(cfg *config.Config) *cli.Command {
 			if err != nil {
 				logger.Fatal().Err(err).Str("addr", cfg.Notifications.RevaGateway).Msg("could not get reva client")
 			}
-			valueService := settingssvc.NewValueService("com.owncloud.api.settings", grpc.DefaultClient())
-			svc := service.NewEventsNotifier(evts, channel, logger, gwclient, valueService, cfg.Notifications.MachineAuthAPIKey, cfg.Notifications.EmailTemplatePath, cfg.WebUIURL)
+
+			svc := service.NewEventsNotifier(evts, channel, logger, gwclient, cfg.Notifications.MachineAuthAPIKey, cfg.Notifications.EmailTemplatePath, cfg.WebUIURL)
 			return svc.Run()
 		},
 	}
diff --git a/services/notifications/pkg/service/service.go b/services/notifications/pkg/service/service.go
index e20b917c283..714696e720f 100644
--- a/services/notifications/pkg/service/service.go
+++ b/services/notifications/pkg/service/service.go
@@ -17,17 +17,11 @@ import (
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	"github.com/cs3org/reva/v2/pkg/events"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
-	"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
-	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	"github.com/owncloud/ocis/v2/services/notifications/pkg/channels"
 	"github.com/owncloud/ocis/v2/services/notifications/pkg/email"
-	"github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults"
-	"go-micro.dev/v4/metadata"
 	"google.golang.org/protobuf/types/known/fieldmaskpb"
 )
 
-var _defaultLocale = "en"
-
 // Service should be named `Runner`
 type Service interface {
 	Run() error
@@ -39,16 +33,13 @@ func NewEventsNotifier(
 	channel channels.Channel,
 	logger log.Logger,
 	gwClient gateway.GatewayAPIClient,
-	valueService settingssvc.ValueService,
 	machineAuthAPIKey, emailTemplatePath, ocisURL string) Service {
-
 	return eventsNotifier{
 		logger:            logger,
 		channel:           channel,
 		events:            events,
 		signals:           make(chan os.Signal, 1),
 		gwClient:          gwClient,
-		valueService:      valueService,
 		machineAuthAPIKey: machineAuthAPIKey,
 		emailTemplatePath: emailTemplatePath,
 		ocisURL:           ocisURL,
@@ -61,7 +52,6 @@ type eventsNotifier struct {
 	events            <-chan events.Event
 	signals           chan os.Signal
 	gwClient          gateway.GatewayAPIClient
-	valueService      settingssvc.ValueService
 	machineAuthAPIKey string
 	emailTemplatePath string
 	translationPath   string
@@ -97,101 +87,52 @@ func (s eventsNotifier) Run() error {
 	}
 }
 
-// recipient represent the already rendered message including the user id opaqueID
-type recipient struct {
-	opaqueID string
-	subject  string
-	msg      string
+func (s eventsNotifier) render(template email.MessageTemplate, values map[string]interface{}) (string, string, error) {
+	// The locate have to come from the user setting
+	return email.RenderEmailTemplate(template, "en", s.emailTemplatePath, s.translationPath, values)
 }
 
-func (s eventsNotifier) render(ctx context.Context, template email.MessageTemplate,
-	granteeFieldName string, fields map[string]interface{}, granteeList []*user.UserId) ([]recipient, error) {
-	// Render the Email Template for each user
-	recipientList := make([]recipient, len(granteeList))
-	for i, userID := range granteeList {
-		locale, err := s.getUserLang(ctx, userID)
-		if err != nil {
-			return nil, err
-		}
-		grantee, err := s.getUserName(ctx, userID)
-		if err != nil {
-			return nil, err
-		}
-		fields[granteeFieldName] = grantee
+func (s eventsNotifier) send(ctx context.Context, u *user.UserId, g *group.GroupId, msg, subj, sender string) error {
+	if u != nil {
+		return s.channel.SendMessage(ctx, []string{u.GetOpaqueId()}, msg, subj, sender)
 
-		subj, msg, err := email.RenderEmailTemplate(template, locale, s.emailTemplatePath, s.translationPath, fields)
-		if err != nil {
-			return nil, err
-		}
-		recipientList[i] = recipient{opaqueID: userID.GetOpaqueId(), subject: subj, msg: msg}
 	}
-	return recipientList, nil
-}
 
-func (s eventsNotifier) send(ctx context.Context, recipientList []recipient, sender string) {
-	for _, r := range recipientList {
-		err := s.channel.SendMessage(ctx, []string{r.opaqueID}, r.msg, r.subject, sender)
-		if err != nil {
-			s.logger.Error().Err(err).Str("event", "SendEmail").Msg("failed to send a message")
-		}
+	if g != nil {
+		return s.channel.SendMessageToGroup(ctx, g, msg, subj, sender)
 	}
+
+	return nil
 }
 
-func (s eventsNotifier) getGranteeList(ctx context.Context, owner, u *user.UserId, g *group.GroupId) ([]*user.UserId, error) {
+func (s eventsNotifier) getGranteeName(ctx context.Context, u *user.UserId, g *group.GroupId) (string, error) {
 	switch {
 	case u != nil:
-		return []*user.UserId{u}, nil
-	case g != nil:
-		res, err := s.gwClient.GetGroup(ctx, &group.GetGroupRequest{GroupId: g})
+		r, err := s.gwClient.GetUser(ctx, &user.GetUserRequest{UserId: u})
 		if err != nil {
-			return nil, err
-		}
-		if res.Status.Code != rpc.Code_CODE_OK {
-			return nil, errors.New("could not get group")
+			return "", err
 		}
-		for i, userID := range res.GetGroup().GetMembers() {
-			// remove an executant from a list
-			if userID.GetOpaqueId() == owner.GetOpaqueId() {
-				res.Group.Members[i] = res.Group.Members[len(res.Group.Members)-1]
-				return res.Group.Members[:len(res.Group.Members)-1], nil
-			}
-		}
-		return res.Group.Members, nil
-	default:
-		return nil, errors.New("need at least one non-nil grantee")
-	}
-}
 
-func (s eventsNotifier) getUserName(ctx context.Context, u *user.UserId) (string, error) {
-	if u == nil {
-		return "", errors.New("need at least one non-nil grantee")
-	}
-	r, err := s.gwClient.GetUser(ctx, &user.GetUserRequest{UserId: u})
-	if err != nil {
-		return "", err
-	}
-	if r.Status.Code != rpc.Code_CODE_OK {
-		return "", fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode())
-	}
-	return r.GetUser().GetDisplayName(), nil
-}
+		if r.Status.Code != rpc.Code_CODE_OK {
+			return "", fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode())
+		}
 
-func (s eventsNotifier) getUserLang(ctx context.Context, u *user.UserId) (string, error) {
-	granteeCtx := metadata.Set(ctx, middleware.AccountID, u.OpaqueId)
-	if resp, err := s.valueService.GetValueByUniqueIdentifiers(granteeCtx,
-		&settingssvc.GetValueByUniqueIdentifiersRequest{
-			AccountUuid: u.OpaqueId,
-			SettingId:   defaults.SettingUUIDProfileLanguage,
-		}); err == nil {
-		if resp == nil {
-			return _defaultLocale, nil
+		return r.GetUser().GetDisplayName(), nil
+	case g != nil:
+		r, err := s.gwClient.GetGroup(ctx, &group.GetGroupRequest{GroupId: g})
+		if err != nil {
+			return "", err
 		}
-		val := resp.Value.GetValue().GetListValue().GetValues()
-		if len(val) > 0 && val[0] != nil {
-			return val[0].GetStringValue(), nil
+
+		if r.GetStatus().GetCode() != rpc.Code_CODE_OK {
+			return "", fmt.Errorf("unexpected status code from gateway client: %d", r.GetStatus().GetCode())
 		}
+
+		return r.GetGroup().GetDisplayName(), nil
+	default:
+		return "", errors.New("Need at least one non-nil grantee")
 	}
-	return _defaultLocale, nil
+
 }
 
 func (s eventsNotifier) getResourceInfo(ctx context.Context, resourceID *provider.ResourceId, fieldmask *fieldmaskpb.FieldMask) (*provider.ResourceInfo, error) {
diff --git a/services/notifications/pkg/service/shares.go b/services/notifications/pkg/service/shares.go
index 4c8d8444cf5..6451783bc2f 100644
--- a/services/notifications/pkg/service/shares.go
+++ b/services/notifications/pkg/service/shares.go
@@ -35,25 +35,28 @@ func (s eventsNotifier) handleShareCreated(e events.ShareCreated) {
 		return
 	}
 
-	granteeList, err := s.getGranteeList(ownerCtx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
+	shareGrantee, err := s.getGranteeName(ownerCtx, e.GranteeUserID, e.GranteeGroupID)
 	if err != nil {
-		s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not get grantee list")
+		s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not get grantee name")
 		return
 	}
 
 	sharerDisplayName := owner.GetDisplayName()
-	recipientList, err := s.render(ownerCtx, email.ShareCreated,
-		"ShareGrantee",
-		map[string]interface{}{
-			"ShareSharer": sharerDisplayName,
-			"ShareFolder": resourceInfo.Name,
-			"ShareLink":   shareLink,
-		}, granteeList)
+	subj, msg, err := s.render(email.ShareCreated, map[string]interface{}{
+		"ShareGrantee": shareGrantee,
+		"ShareSharer":  sharerDisplayName,
+		"ShareFolder":  resourceInfo.Name,
+		"ShareLink":    shareLink,
+	})
+
 	if err != nil {
-		s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("could not get render the email")
-		return
+		s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not render E-Mail body template for shares")
+	}
+
+	if err := s.send(ownerCtx, e.GranteeUserID, e.GranteeGroupID, msg, subj, sharerDisplayName); err != nil {
+		s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("failed to send a message")
 	}
-	s.send(ownerCtx, recipientList, sharerDisplayName)
+
 }
 
 func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
@@ -62,13 +65,13 @@ func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
 		Str("itemid", e.ItemID.GetOpaqueId()).
 		Logger()
 
-	ownerCtx, owner, err := utils.Impersonate(e.ShareOwner, s.gwClient, s.machineAuthAPIKey)
+	ctx, owner, err := utils.Impersonate(e.ShareOwner, s.gwClient, s.machineAuthAPIKey)
 	if err != nil {
 		logger.Error().Err(err).Msg("Could not impersonate sharer")
 		return
 	}
 
-	resourceInfo, err := s.getResourceInfo(ownerCtx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
+	resourceInfo, err := s.getResourceInfo(ctx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
 	if err != nil {
 		logger.Error().
 			Err(err).
@@ -76,21 +79,24 @@ func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
 		return
 	}
 
-	granteeList, err := s.getGranteeList(ownerCtx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
+	shareGrantee, err := s.getGranteeName(ctx, e.GranteeUserID, e.GranteeGroupID)
 	if err != nil {
-		s.logger.Error().Err(err).Str("event", "ShareExpired").Msg("Could not get grantee name")
+		s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not get grantee name")
 		return
 	}
 
-	recipientList, err := s.render(ownerCtx, email.ShareExpired,
-		"ShareGrantee",
-		map[string]interface{}{
-			"ShareFolder": resourceInfo.GetName(),
-			"ExpiredAt":   e.ExpiredAt.Format("2006-01-02 15:04:05"),
-		}, granteeList)
+	subj, msg, err := s.render(email.ShareExpired, map[string]interface{}{
+		"ShareGrantee": shareGrantee,
+		"ShareFolder":  resourceInfo.GetName(),
+		"ExpiredAt":    e.ExpiredAt.Format("2006-01-02 15:04:05"),
+	})
+
 	if err != nil {
-		s.logger.Error().Err(err).Str("event", "ShareExpired").Msg("could not get render the email")
-		return
+		s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not render E-Mail body template for shares")
 	}
-	s.send(ownerCtx, recipientList, owner.GetDisplayName())
+
+	if err := s.send(ctx, e.GranteeUserID, e.GranteeGroupID, msg, subj, owner.GetDisplayName()); err != nil {
+		s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("failed to send a message")
+	}
+
 }
diff --git a/services/notifications/pkg/service/spaces.go b/services/notifications/pkg/service/spaces.go
index 78f6e59359d..bf7d7473428 100644
--- a/services/notifications/pkg/service/spaces.go
+++ b/services/notifications/pkg/service/spaces.go
@@ -13,7 +13,7 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
 		Str("itemid", e.ID.OpaqueId).
 		Logger()
 
-	executantCtx, executant, err := utils.Impersonate(e.Executant, s.gwClient, s.machineAuthAPIKey)
+	ownerCtx, owner, err := utils.Impersonate(e.Executant, s.gwClient, s.machineAuthAPIKey)
 	if err != nil {
 		logger.Error().
 			Err(err).
@@ -29,7 +29,7 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
 		return
 	}
 
-	resourceInfo, err := s.getResourceInfo(executantCtx, &resourceID, nil)
+	resourceInfo, err := s.getResourceInfo(ownerCtx, &resourceID, nil)
 	if err != nil {
 		logger.Error().
 			Err(err).
@@ -45,28 +45,30 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
 		return
 	}
 
-	// Note: We're using the 'executantCtx' (authenticated as the share executant) here for requesting
+	// Note: We're using the 'ownerCtx' (authenticated as the share owner) here for requesting
 	// the Grantees of the shares. Ideally the notfication service would use some kind of service
 	// user for this.
-	spaceGrantee, err := s.getGranteeList(executantCtx, executant.GetId(), e.GranteeUserID, e.GranteeGroupID)
+	spaceGrantee, err := s.getGranteeName(ownerCtx, e.GranteeUserID, e.GranteeGroupID)
 	if err != nil {
-		logger.Error().Err(err).Str("event", "SpaceGrantee").Msg("Could not get grantee list")
+		logger.Error().Err(err).Msg("Could not get grantee name")
 		return
 	}
 
-	sharerDisplayName := executant.GetDisplayName()
-	recipientList, err := s.render(executantCtx, email.SharedSpace,
-		"SpaceGrantee",
-		map[string]interface{}{
-			"SpaceSharer": sharerDisplayName,
-			"SpaceName":   resourceInfo.GetSpace().GetName(),
-			"ShareLink":   shareLink,
-		}, spaceGrantee)
+	sharerDisplayName := owner.GetDisplayName()
+	subj, msg, err := s.render(email.SharedSpace, map[string]interface{}{
+		"SpaceGrantee": spaceGrantee,
+		"SpaceSharer":  sharerDisplayName,
+		"SpaceName":    resourceInfo.GetSpace().GetName(),
+		"ShareLink":    shareLink,
+	})
 	if err != nil {
-		s.logger.Error().Err(err).Str("event", "SharedSpace").Msg("could not get render the email")
+		logger.Error().Err(err).Msg("Could not render E-Mail template for spaces")
 		return
 	}
-	s.send(executantCtx, recipientList, sharerDisplayName)
+
+	if err := s.send(ownerCtx, e.GranteeUserID, e.GranteeGroupID, msg, subj, sharerDisplayName); err != nil {
+		logger.Error().Err(err).Msg("failed to send a message")
+	}
 }
 
 func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
@@ -75,7 +77,7 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
 		Str("itemid", e.ID.OpaqueId).
 		Logger()
 
-	executantCtx, executant, err := utils.Impersonate(e.Executant, s.gwClient, s.machineAuthAPIKey)
+	ownerCtx, owner, err := utils.Impersonate(e.Executant, s.gwClient, s.machineAuthAPIKey)
 	if err != nil {
 		logger.Error().Err(err).Msg("could not handle space unshared event")
 		return
@@ -89,7 +91,7 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
 		return
 	}
 
-	resourceInfo, err := s.getResourceInfo(executantCtx, &resourceID, nil)
+	resourceInfo, err := s.getResourceInfo(ownerCtx, &resourceID, nil)
 	if err != nil {
 		logger.Error().
 			Err(err).
@@ -105,28 +107,31 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
 		return
 	}
 
-	// Note: We're using the 'executantCtx' (authenticated as the share executant) here for requesting
+	// Note: We're using the 'ownerCtx' (authenticated as the share owner) here for requesting
 	// the Grantees of the shares. Ideally the notfication service would use some kind of service
 	// user for this.
-	spaceGrantee, err := s.getGranteeList(executantCtx, executant.GetId(), e.GranteeUserID, e.GranteeGroupID)
+	spaceGrantee, err := s.getGranteeName(ownerCtx, e.GranteeUserID, e.GranteeGroupID)
 	if err != nil {
-		logger.Error().Err(err).Str("event", "SpaceGrantee").Msg("Could not get grantee list")
+		logger.Error().Err(err).Msg("Could not get grantee name")
 		return
 	}
 
-	sharerDisplayName := executant.GetDisplayName()
-	recipientList, err := s.render(executantCtx, email.UnsharedSpace,
-		"SpaceGrantee",
-		map[string]interface{}{
-			"SpaceSharer": sharerDisplayName,
-			"SpaceName":   resourceInfo.GetSpace().Name,
-			"ShareLink":   shareLink,
-		}, spaceGrantee)
+	sharerDisplayName := owner.GetDisplayName()
+	subj, msg, err := s.render(email.UnsharedSpace, map[string]interface{}{
+		"SpaceGrantee": spaceGrantee,
+		"SpaceSharer":  sharerDisplayName,
+		"SpaceName":    resourceInfo.GetSpace().Name,
+		"ShareLink":    shareLink,
+	})
+
 	if err != nil {
-		s.logger.Error().Err(err).Str("event", "UnsharedSpace").Msg("Could not get render the email")
+		logger.Error().Err(err).Msg("Could not render E-Mail template for spaces")
 		return
 	}
-	s.send(executantCtx, recipientList, sharerDisplayName)
+
+	if err := s.send(ownerCtx, e.GranteeUserID, e.GranteeGroupID, msg, subj, sharerDisplayName); err != nil {
+		logger.Error().Err(err).Msg("failed to send a message")
+	}
 }
 
 func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExpired) {
@@ -135,27 +140,30 @@ func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExp
 		Str("itemid", e.SpaceID.GetOpaqueId()).
 		Logger()
 
-	ownerCtx, owner, err := utils.Impersonate(e.SpaceOwner, s.gwClient, s.machineAuthAPIKey)
+	ctx, owner, err := utils.Impersonate(e.SpaceOwner, s.gwClient, s.machineAuthAPIKey)
 	if err != nil {
 		logger.Error().Err(err).Msg("Could not impersonate sharer")
 		return
 	}
 
-	granteeList, err := s.getGranteeList(ownerCtx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
+	shareGrantee, err := s.getGranteeName(ctx, e.GranteeUserID, e.GranteeGroupID)
 	if err != nil {
-		s.logger.Error().Err(err).Str("event", "SpaceUnshared").Msg("Could not get grantee list")
+		s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not get grantee name")
 		return
 	}
 
-	recipientList, err := s.render(ownerCtx, email.MembershipExpired,
-		"SpaceGrantee",
-		map[string]interface{}{
-			"SpaceName": e.SpaceName,
-			"ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"),
-		}, granteeList)
+	subj, msg, err := s.render(email.MembershipExpired, map[string]interface{}{
+		"SpaceGrantee": shareGrantee,
+		"SpaceName":    e.SpaceName,
+		"ExpiredAt":    e.ExpiredAt.Format("2006-01-02 15:04:05"),
+	})
+
 	if err != nil {
-		s.logger.Error().Err(err).Str("event", "SpaceUnshared").Msg("could not get render the email")
-		return
+		s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("Could not render E-Mail body template for shares")
 	}
-	s.send(ownerCtx, recipientList, owner.GetDisplayName())
+
+	if err := s.send(ctx, e.GranteeUserID, e.GranteeGroupID, msg, subj, owner.GetDisplayName()); err != nil {
+		s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("failed to send a message")
+	}
+
 }
diff --git a/services/ocs/README.md b/services/ocs/README.md
index 5e2135ab45a..ca1e16943b6 100644
--- a/services/ocs/README.md
+++ b/services/ocs/README.md
@@ -7,13 +7,13 @@ The ocs service is an ...
 The `ocs` service can use a configured store via `OCS_STORE_TYPE`. Possible stores are:
   -   `memory`: Basic in-memory store and the default.
   -   `ocmem`: Advanced in-memory store allowing max size.
-  -   `redis`: Stores data in a configured redis cluster.
-  -   `redis-sentinel`: Stores data in a configured redis sentinel cluster.
+  -   `redis`: Stores data in a configured Redis cluster.
+  -   `redis-sentinel`: Stores data in a configured Redis Sentinel cluster.
   -   `etcd`: Stores data in a configured etcd cluster.
   -   `nats-js`: Stores data using key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
   -   `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
 
-1.  Note that in-memory stores are by nature not reboot persistent.
-2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store applies.
+1.  Note that in-memory stores are by nature not reboot-persistent.
+2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store apply.
 3.  The ocs service can be scaled if not using `in-memory` stores and the stores are configured identically over all instances.
 4.  When using `redis-sentinel`, the Redis master to use is configured via `OCS_CACHE_STORE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
diff --git a/services/proxy/README.md b/services/proxy/README.md
index 09c34ed4382..8d27bc07e7d 100644
--- a/services/proxy/README.md
+++ b/services/proxy/README.md
@@ -98,13 +98,13 @@ In a production deployment, you want to have basic authentication (`PROXY_ENABLE
 The `proxy` service can use a configured store via `PROXY_STORE_TYPE`. Possible stores are:
   -   `memory`: Basic in-memory store and the default.
   -   `ocmem`: Advanced in-memory store allowing max size.
-  -   `redis`: Stores data in a configured redis cluster.
-  -   `redis-sentinel`: Stores data in a configured redis sentinel cluster.
+  -   `redis`: Stores data in a configured Redis cluster.
+  -   `redis-sentinel`: Stores data in a configured Redis Sentinel cluster.
   -   `etcd`: Stores data in a configured etcd cluster.
   -   `nats-js`: Stores data using key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
   -   `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
 
-1.  Note that in-memory stores are by nature not reboot persistent.
-2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store applies.
+1.  Note that in-memory stores are by nature not reboot-persistent.
+2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store apply.
 3.  The proxy service can be scaled if not using `in-memory` stores and the stores are configured identically over all instances.
 4.  When using `redis-sentinel`, the Redis master to use is configured via `PROXY_OIDC_USERINFO_CACHE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
diff --git a/services/settings/pkg/service/v0/settings.go b/services/settings/pkg/service/v0/settings.go
index ab58b915ff4..411e6310bfc 100644
--- a/services/settings/pkg/service/v0/settings.go
+++ b/services/settings/pkg/service/v0/settings.go
@@ -43,8 +43,7 @@ const (
 	// CreateSpacePermissionName is the hardcoded setting name for the create space permission
 	CreateSpacePermissionName string = "create-space"
 
-	// SettingUUIDProfileLanguage is the hardcoded setting UUID for the user profile language
-	SettingUUIDProfileLanguage = "aa8cfbe5-95d4-4f7e-a032-c3c01f5f062f"
+	settingUUIDProfileLanguage = "aa8cfbe5-95d4-4f7e-a032-c3c01f5f062f"
 
 	// AccountManagementPermissionID is the hardcoded setting UUID for the account management permission
 	AccountManagementPermissionID string = "8e587774-d929-4215-910b-a317b1e80f73"
@@ -208,7 +207,7 @@ func generateBundleProfileRequest() *settingsmsg.Bundle {
 		DisplayName: "Profile",
 		Settings: []*settingsmsg.Setting{
 			{
-				Id:          SettingUUIDProfileLanguage,
+				Id:          settingUUIDProfileLanguage,
 				Name:        "language",
 				DisplayName: "Language",
 				Description: "User language",
@@ -269,7 +268,7 @@ func generatePermissionRequests() []*settingssvc.AddSettingToBundleRequest {
 				DisplayName: "Permission to read and set the language (anyone)",
 				Resource: &settingsmsg.Resource{
 					Type: settingsmsg.Resource_TYPE_SETTING,
-					Id:   SettingUUIDProfileLanguage,
+					Id:   settingUUIDProfileLanguage,
 				},
 				Value: &settingsmsg.Setting_PermissionValue{
 					PermissionValue: &settingsmsg.Permission{
@@ -287,7 +286,7 @@ func generatePermissionRequests() []*settingssvc.AddSettingToBundleRequest {
 				DisplayName: "Permission to read and set the language (self)",
 				Resource: &settingsmsg.Resource{
 					Type: settingsmsg.Resource_TYPE_SETTING,
-					Id:   SettingUUIDProfileLanguage,
+					Id:   settingUUIDProfileLanguage,
 				},
 				Value: &settingsmsg.Setting_PermissionValue{
 					PermissionValue: &settingsmsg.Permission{
@@ -305,7 +304,7 @@ func generatePermissionRequests() []*settingssvc.AddSettingToBundleRequest {
 				DisplayName: "Permission to read and set the language (self)",
 				Resource: &settingsmsg.Resource{
 					Type: settingsmsg.Resource_TYPE_SETTING,
-					Id:   SettingUUIDProfileLanguage,
+					Id:   settingUUIDProfileLanguage,
 				},
 				Value: &settingsmsg.Setting_PermissionValue{
 					PermissionValue: &settingsmsg.Permission{
@@ -524,7 +523,7 @@ func generatePermissionRequests() []*settingssvc.AddSettingToBundleRequest {
 				DisplayName: "Permission to read and set the language (self)",
 				Resource: &settingsmsg.Resource{
 					Type: settingsmsg.Resource_TYPE_SETTING,
-					Id:   SettingUUIDProfileLanguage,
+					Id:   settingUUIDProfileLanguage,
 				},
 				Value: &settingsmsg.Setting_PermissionValue{
 					PermissionValue: &settingsmsg.Permission{
diff --git a/services/settings/pkg/store/defaults/defaults.go b/services/settings/pkg/store/defaults/defaults.go
index 6e7b463f6b5..534da706f1c 100644
--- a/services/settings/pkg/store/defaults/defaults.go
+++ b/services/settings/pkg/store/defaults/defaults.go
@@ -73,8 +73,7 @@ const (
 	// SpaceAbilityPermissionName is the hardcoded setting name for the space ability permission
 	SpaceAbilityPermissionName string = "Drive.ReadWriteEnabled"
 
-	// SettingUUIDProfileLanguage is the hardcoded setting UUID for the user profile language
-	SettingUUIDProfileLanguage = "aa8cfbe5-95d4-4f7e-a032-c3c01f5f062f"
+	settingUUIDProfileLanguage = "aa8cfbe5-95d4-4f7e-a032-c3c01f5f062f"
 
 	// AccountManagementPermissionID is the hardcoded setting UUID for the account management permission
 	AccountManagementPermissionID string = "8e587774-d929-4215-910b-a317b1e80f73"
@@ -160,7 +159,7 @@ func generateBundleAdminRole() *settingsmsg.Bundle {
 				DisplayName: "Permission to read and set the language (anyone)",
 				Resource: &settingsmsg.Resource{
 					Type: settingsmsg.Resource_TYPE_SETTING,
-					Id:   SettingUUIDProfileLanguage,
+					Id:   settingUUIDProfileLanguage,
 				},
 				Value: &settingsmsg.Setting_PermissionValue{
 					PermissionValue: &settingsmsg.Permission{
@@ -417,7 +416,7 @@ func generateBundleSpaceAdminRole() *settingsmsg.Bundle {
 				DisplayName: "Permission to read and set the language (self)",
 				Resource: &settingsmsg.Resource{
 					Type: settingsmsg.Resource_TYPE_SETTING,
-					Id:   SettingUUIDProfileLanguage,
+					Id:   settingUUIDProfileLanguage,
 				},
 				Value: &settingsmsg.Setting_PermissionValue{
 					PermissionValue: &settingsmsg.Permission{
@@ -493,7 +492,7 @@ func generateBundleUserRole() *settingsmsg.Bundle {
 				DisplayName: "Permission to read and set the language (self)",
 				Resource: &settingsmsg.Resource{
 					Type: settingsmsg.Resource_TYPE_SETTING,
-					Id:   SettingUUIDProfileLanguage,
+					Id:   settingUUIDProfileLanguage,
 				},
 				Value: &settingsmsg.Setting_PermissionValue{
 					PermissionValue: &settingsmsg.Permission{
@@ -569,7 +568,7 @@ func generateBundleGuestRole() *settingsmsg.Bundle {
 				DisplayName: "Permission to read and set the language (self)",
 				Resource: &settingsmsg.Resource{
 					Type: settingsmsg.Resource_TYPE_SETTING,
-					Id:   SettingUUIDProfileLanguage,
+					Id:   settingUUIDProfileLanguage,
 				},
 				Value: &settingsmsg.Setting_PermissionValue{
 					PermissionValue: &settingsmsg.Permission{
@@ -594,7 +593,7 @@ func generateBundleProfileRequest() *settingsmsg.Bundle {
 		DisplayName: "Profile",
 		Settings: []*settingsmsg.Setting{
 			{
-				Id:          SettingUUIDProfileLanguage,
+				Id:          settingUUIDProfileLanguage,
 				Name:        "language",
 				DisplayName: "Language",
 				Description: "User language",
diff --git a/services/settings/pkg/store/metadata/values.go b/services/settings/pkg/store/metadata/values.go
index c40f859fa73..a9df99dacea 100644
--- a/services/settings/pkg/store/metadata/values.go
+++ b/services/settings/pkg/store/metadata/values.go
@@ -4,6 +4,7 @@ package store
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 
 	"github.com/cs3org/reva/v2/pkg/errtypes"
@@ -86,39 +87,8 @@ func (s *Store) ReadValue(valueID string) (*settingsmsg.Value, error) {
 
 // ReadValueByUniqueIdentifiers tries to find a value given a set of unique identifiers
 func (s *Store) ReadValueByUniqueIdentifiers(accountUUID, settingID string) (*settingsmsg.Value, error) {
-	if settingID == "" {
-		return nil, fmt.Errorf("settingID can not be empty %w", settings.ErrNotFound)
-	}
-	s.Init()
-	ctx := context.TODO()
-
-	vIDs, err := s.mdc.ReadDir(ctx, valuesFolderLocation)
-	if err != nil {
-		return nil, err
-	}
-
-	for _, vid := range vIDs {
-		b, err := s.mdc.SimpleDownload(ctx, valuePath(vid))
-		switch err.(type) {
-		case nil:
-			// continue
-		case errtypes.NotFound:
-			continue
-		default:
-			return nil, err
-		}
-
-		v := &settingsmsg.Value{}
-		err = json.Unmarshal(b, v)
-		if err != nil {
-			return nil, err
-		}
-
-		if v.AccountUuid == accountUUID && v.SettingId == settingID {
-			return v, nil
-		}
-	}
-	return nil, settings.ErrNotFound
+	fmt.Println("ReadValueByUniqueIdentifiers not implemented")
+	return nil, errors.New("not implemented")
 }
 
 // WriteValue writes the given value into a file within the dataPath
diff --git a/services/storage-system/README.md b/services/storage-system/README.md
index 9bf2cc6d0a3..d425acee5f9 100644
--- a/services/storage-system/README.md
+++ b/services/storage-system/README.md
@@ -12,7 +12,7 @@ The `storage-system` service caches file metadata via the configured store in `S
   -   `nats-js`: Stores metadata using the key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
   -   `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
 
-1.  Note that in-memory stores are by nature not reboot persistent.
-2.  Though usually not necessary, a database name can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`, `redis` and `redis-sentinel`. These settings are blank by default which means that the standard settings of the configured store applies.
+1.  Note that in-memory stores are by nature not reboot-persistent.
+2.  Though usually not necessary, a database name can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`, `redis` and `redis-sentinel`. These settings are blank by default which means that the standard settings of the configured store apply.
 3.  The `storage-system` service can be scaled if not using `in-memory` stores and the stores are configured identically over all instances.
 4.  When using `redis-sentinel`, the Redis master to use is configured via `STORAGE_SYSTEM_CACHE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
diff --git a/services/storage-users/README.md b/services/storage-users/README.md
index 4896e210f8b..8952368ce17 100644
--- a/services/storage-users/README.md
+++ b/services/storage-users/README.md
@@ -83,7 +83,7 @@ The configuration for the `purge-expired` command is done by using the following
 
 ## Caching
 
-The `storage-users` service caches file metadata via the configured store in `STORAGE_USERS_CACHE_STORE`. Possible stores are:
+The `storage-users` service caches stat and metadata via the configured store in `STORAGE_USERS_STAT_CACHE_STORE` and `STORAGE_USERS_FILEMETADATA_CACHE_STORE`. Possible stores are:
   -   `memory`: Basic in-memory store and the default.
   -   `redis`: Stores metadata in a configured Redis cluster.
   -   `redis-sentinel`: Stores metadata in a configured Redis Sentinel cluster.
@@ -91,7 +91,7 @@ The `storage-users` service caches file metadata via the configured store in `ST
   -   `nats-js`: Stores metadata using the key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
   -   `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
 
-1.  Note that in-memory stores are by nature not reboot persistent.
-2.  Though usually not necessary, a database name can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`, `redis` and `redis-sentinel`. These settings are blank by default which means that the standard settings of the configured store applies.
+1.  Note that in-memory stores are by nature not reboot-persistent.
+2.  Though usually not necessary, a database name can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`, `redis` and `redis-sentinel`. These settings are blank by default which means that the standard settings of the configured store apply.
 3.  The `storage-users` service can be scaled if not using `in-memory` stores and the stores are configured identically over all instances.
-4.  When using `redis-sentinel`, the Redis master to use is configured via `STORAGE_SYSTEM_CACHE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
+4.  When using `redis-sentinel`, the Redis master to use is configured via `STORAGE_USERS_STAT_CACHE_STORE_NODES` and `STORAGE_USERS_FILEMETADATA_CACHE_STORE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
diff --git a/services/userlog/README.md b/services/userlog/README.md
index 83548538a53..c509163df13 100644
--- a/services/userlog/README.md
+++ b/services/userlog/README.md
@@ -11,14 +11,14 @@ Running the `userlog` service without running the `eventhistory` service is not
 The `userlog` service persists information via the configured store in `USERLOG_STORE_TYPE`. Possible stores are:
   -   `memory`: Basic in-memory store and the default.
   -   `ocmem`: Advanced in-memory store allowing max size.
-  -   `redis`: Stores data in a configured redis cluster.
-  -   `redis-sentinel`: Stores data in a configured redis sentinel cluster.
+  -   `redis`: Stores data in a configured Redis cluster.
+  -   `redis-sentinel`: Stores data in a configured Redis Sentinel cluster.
   -   `etcd`: Stores data in a configured etcd cluster.
   -   `nats-js`: Stores data using key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
   -   `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
 
-1.  Note that in-memory stores are by nature not reboot persistent.
-2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store applies.
+1.  Note that in-memory stores are by nature not reboot-persistent.
+2.  Though usually not necessary, a database name and a database table can be configured for event stores if the event store supports this. Generally not applicable for stores of type `in-memory`. These settings are blank by default which means that the standard settings of the configured store apply.
 3.  The userlog service can be scaled if not using `in-memory` stores and the stores are configured identically over all instances.
 4.  When using `redis-sentinel`, the Redis master to use is configured via `USERLOG_STORE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
 
diff --git a/services/userlog/pkg/service/service.go b/services/userlog/pkg/service/service.go
index 8429919a497..deda66be728 100644
--- a/services/userlog/pkg/service/service.go
+++ b/services/userlog/pkg/service/service.go
@@ -352,10 +352,6 @@ func (ul *UserlogService) resolveID(ctx context.Context, userid *user.UserId, gr
 		return []string{userid.GetOpaqueId()}, nil
 	}
 
-	if ctx == nil {
-		return nil, errors.New("need ctx to resolve group id")
-	}
-
 	return ul.resolveGroup(ctx, groupid.GetOpaqueId())
 }
 


Test patch:
diff --git a/services/notifications/pkg/service/service_test.go b/services/notifications/pkg/service/service_test.go
index 4f6017eded5..aad56181995 100644
--- a/services/notifications/pkg/service/service_test.go
+++ b/services/notifications/pkg/service/service_test.go
@@ -15,19 +15,13 @@ import (
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
-	ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
-	"github.com/owncloud/ocis/v2/ocis-pkg/shared"
-	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
-	"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
 	"github.com/owncloud/ocis/v2/services/notifications/pkg/service"
 	"github.com/test-go/testify/mock"
-	"go-micro.dev/v4/client"
 )
 
 var _ = Describe("Notifications", func() {
 	var (
 		gwc    *cs3mocks.GatewayAPIClient
-		vs     *settingssvc.MockValueService
 		sharer = &user.User{
 			Id: &user.UserId{
 				OpaqueId: "sharer",
@@ -49,23 +43,15 @@ var _ = Describe("Notifications", func() {
 
 	BeforeEach(func() {
 		gwc = &cs3mocks.GatewayAPIClient{}
-		gwc.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil).Once()
-		gwc.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil).Once()
+		gwc.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil)
 		gwc.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil)
 		gwc.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, Info: &provider.ResourceInfo{Name: "secrets of the board", Space: &provider.StorageSpace{Name: "secret space"}}}, nil)
-		vs = &settingssvc.MockValueService{}
-		vs.GetValueByUniqueIdentifiersFunc = func(ctx context.Context, req *settingssvc.GetValueByUniqueIdentifiersRequest, opts ...client.CallOption) (*settingssvc.GetValueResponse, error) {
-			return nil, nil
-		}
 	})
 
 	DescribeTable("Sending notifications",
 		func(tc testChannel, ev events.Event) {
-			cfg := defaults.FullDefaultConfig()
-			cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
-			_ = ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...)
 			ch := make(chan events.Event)
-			evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gwc, vs, "", "", "")
+			evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gwc, "", "", "")
 			go evts.Run()
 
 			ch <- ev
@@ -80,7 +66,7 @@ var _ = Describe("Notifications", func() {
 		Entry("Share Created", testChannel{
 			expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
 			expectedSubject:     "Dr. S. Harer shared 'secrets of the board' with you",
-			expectedMessage: `Hello Eric Expireling
+			expectedMessage: `Hello Dr. S. Harer
 
 Dr. S. Harer has shared "secrets of the board" with you.
 
@@ -105,7 +91,7 @@ https://owncloud.com
 		Entry("Share Expired", testChannel{
 			expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
 			expectedSubject:     "Share to 'secrets of the board' expired at 2023-04-17 16:42:00",
-			expectedMessage: `Hello Eric Expireling,
+			expectedMessage: `Hello Dr. S. Harer,
 
 Your share to secrets of the board has expired at 2023-04-17 16:42:00
 
@@ -130,7 +116,7 @@ https://owncloud.com
 		Entry("Added to Space", testChannel{
 			expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
 			expectedSubject:     "Dr. S. Harer invited you to join secret space",
-			expectedMessage: `Hello Eric Expireling,
+			expectedMessage: `Hello Dr. S. Harer,
 
 Dr. S. Harer has invited you to join "secret space".
 
@@ -155,7 +141,7 @@ https://owncloud.com
 		Entry("Removed from Space", testChannel{
 			expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
 			expectedSubject:     "Dr. S. Harer removed you from secret space",
-			expectedMessage: `Hello Eric Expireling,
+			expectedMessage: `Hello Dr. S. Harer,
 
 Dr. S. Harer has removed you from "secret space".
 
@@ -181,7 +167,7 @@ https://owncloud.com
 		Entry("Space Expired", testChannel{
 			expectedReceipients: map[string]bool{sharee.GetId().GetOpaqueId(): true},
 			expectedSubject:     "Membership of 'secret space' expired at 2023-04-17 16:42:00",
-			expectedMessage: `Hello Eric Expireling,
+			expectedMessage: `Hello Dr. S. Harer,
 
 Your membership of space secret space has expired at 2023-04-17 16:42:00