mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-10-07 11:21:56 -04:00
Add email notifications when a guest link has been created
This commit is contained in:
1 parent
27a0195146
commit
ec7f3f68a4
5 files changed
+211
-13
No files matched your search
@@ -91,6 +91,7 @@ func Server(cfg *config.Config) *cobra.Command {
|
||||
events.ScienceMeshInviteTokenGenerated{},
|
||||
events.SendEmailsEvent{},
|
||||
ocEvents.ResourceMention{},
|
||||
ocEvents.GuestTokenCreated{},
|
||||
}
|
||||
registeredEvents := make(map[string]events.Unmarshaller)
|
||||
for _, e := range evs {
|
||||
|
||||
@@ -49,6 +49,24 @@ Even though this share has been revoked you still might have access through othe
|
||||
Even though this share has been revoked you still might have access through other shares and/or space memberships.`),
|
||||
}
|
||||
|
||||
// Guest link templates
|
||||
GuestLinkShareCreated = MessageTemplate{
|
||||
textTemplate: _textTemplate,
|
||||
htmlTemplate: _htmlTemplate,
|
||||
// GuestLinkShareCreated email template, Subject field (resolves directly)
|
||||
Subject: l10n.Template(`{ShareSharer} shared '{ShareFolder}' with you`),
|
||||
// GuestLinkShareCreated email template, resolves via {{ .Greeting }}
|
||||
Greeting: l10n.Template(`Hello,`),
|
||||
// GuestLinkShareCreated email template, resolves via {{ .MessageBody }}
|
||||
MessageBody: l10n.Template(`{ShareSharer} has shared "{ShareFolder}" with you.
|
||||
|
||||
The link below is personal and can only be used once. Please do not forward it.
|
||||
|
||||
The link is only valid for 30 minutes.`),
|
||||
// GuestLinkShareCreated email template, resolves via {{ .CallToAction }}
|
||||
CallToAction: l10n.Template(`Click here to view it: {ShareLink}`),
|
||||
}
|
||||
|
||||
// Spaces templates
|
||||
SharedSpace = MessageTemplate{
|
||||
textTemplate: _textTemplate,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
|
||||
ocevents "github.com/opencloud-eu/opencloud/pkg/events"
|
||||
"github.com/opencloud-eu/opencloud/services/notifications/pkg/channels"
|
||||
"github.com/opencloud-eu/opencloud/services/notifications/pkg/email"
|
||||
)
|
||||
|
||||
// handleGuestTokenCreated sends the guest link to the guest's email address.
|
||||
// Guests have no account, settings or locale, so the mail is always sent
|
||||
// instantly (the token is short-lived) in the default language.
|
||||
func (s eventsNotifier) handleGuestTokenCreated(e ocevents.GuestTokenCreated) {
|
||||
logger := s.logger.With().
|
||||
Str("event", "GuestTokenCreated").
|
||||
Str("itemid", e.ItemID.GetOpaqueId()).
|
||||
Logger()
|
||||
|
||||
if err := validate.Var(e.GranteeEmail, "required,email"); err != nil {
|
||||
logger.Error().Err(err).Msg("invalid guest email address")
|
||||
return
|
||||
}
|
||||
if e.Token == "" {
|
||||
logger.Error().Msg("guest token is empty")
|
||||
return
|
||||
}
|
||||
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not select next gateway client")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err := utils.GetServiceUserContextWithContext(context.Background(), gatewayClient, s.serviceAccountID, s.serviceAccountSecret)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get service user context")
|
||||
return
|
||||
}
|
||||
|
||||
owner, err := utils.GetUserNoGroups(ctx, e.Sharer, gatewayClient)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get user")
|
||||
return
|
||||
}
|
||||
|
||||
shareFolder := e.ResourceName
|
||||
if shareFolder == "" {
|
||||
resourceInfo, err := s.getResourceInfo(ctx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not stat resource")
|
||||
return
|
||||
}
|
||||
shareFolder = resourceInfo.GetName()
|
||||
}
|
||||
|
||||
shareLink, err := urlJoinPath(s.openCloudURL, "g", e.Token)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not create guest link")
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := email.RenderEmailTemplate(email.GuestLinkShareCreated, s.defaultLanguage, s.defaultLanguage,
|
||||
s.emailTemplatePath, s.translationPath,
|
||||
map[string]string{
|
||||
"ShareSharer": owner.GetDisplayName(),
|
||||
"ShareFolder": shareFolder,
|
||||
"ShareLink": shareLink,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not render the email")
|
||||
return
|
||||
}
|
||||
msg.Sender = owner.GetDisplayName()
|
||||
msg.Recipient = []string{e.GranteeEmail}
|
||||
|
||||
s.send(ctx, []*channels.Message{msg})
|
||||
}
|
||||
@@ -131,6 +131,8 @@ EventLoop:
|
||||
s.handleShareExpired(e, evt.ID)
|
||||
case events.ShareRemoved:
|
||||
s.handleShareRemoved(e, evt.ID)
|
||||
case ocEvents.GuestTokenCreated:
|
||||
s.handleGuestTokenCreated(e)
|
||||
case events.ScienceMeshInviteTokenGenerated:
|
||||
s.handleScienceMeshInviteTokenGenerated(e)
|
||||
case events.SendEmailsEvent:
|
||||
@@ -202,6 +204,10 @@ func (s eventsNotifier) ensureGranteeList(ctx context.Context, executant, u *use
|
||||
func (s eventsNotifier) getGranteeList(ctx context.Context, executant, u *user.UserId, g *group.GroupId) ([]*user.User, error) {
|
||||
switch {
|
||||
case u != nil:
|
||||
// guests aren't users, they are notified via GuestTokenCreated
|
||||
if u.GetType() == user.UserType_USER_TYPE_GUEST {
|
||||
return nil, nil
|
||||
}
|
||||
if s.disableEmails(ctx, u) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
ocEvents "github.com/opencloud-eu/opencloud/pkg/events"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/shared"
|
||||
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
|
||||
@@ -98,7 +99,7 @@ var _ = Describe("Notifications", func() {
|
||||
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
|
||||
ch := make(chan events.Event)
|
||||
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "",
|
||||
"", "", "", "", "", "",
|
||||
"", "", "", testOpenCloudURL, "", "",
|
||||
store.Create(), nil, nil)
|
||||
go evts.Run()
|
||||
|
||||
@@ -118,7 +119,7 @@ var _ = Describe("Notifications", func() {
|
||||
|
||||
Dr. S. Harer has shared "secrets of the board" with you.
|
||||
|
||||
Click here to view it: files/shares/with-me
|
||||
Click here to view it: https://cloud.example.com/files/shares/with-me
|
||||
|
||||
|
||||
---
|
||||
@@ -126,6 +127,7 @@ OpenCloud - a safe home for all your data
|
||||
https://opencloud.eu
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
expectedLink: testOpenCloudURL + "/files/shares/with-me",
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.ShareCreated{
|
||||
@@ -192,7 +194,7 @@ https://opencloud.eu
|
||||
|
||||
Dr. S. Harer has invited you to join "secret space".
|
||||
|
||||
Click here to view it: f/spaceid
|
||||
Click here to view it: https://cloud.example.com/f/spaceid
|
||||
|
||||
|
||||
---
|
||||
@@ -200,6 +202,7 @@ OpenCloud - a safe home for all your data
|
||||
https://opencloud.eu
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
expectedLink: testOpenCloudURL + "/f/spaceid",
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.SpaceShared{
|
||||
@@ -219,7 +222,7 @@ Dr. S. Harer has removed you from "secret space".
|
||||
|
||||
You might still have access through your other groups or direct membership.
|
||||
|
||||
Click here to check it: f/spaceid
|
||||
Click here to check it: https://cloud.example.com/f/spaceid
|
||||
|
||||
|
||||
---
|
||||
@@ -227,6 +230,7 @@ OpenCloud - a safe home for all your data
|
||||
https://opencloud.eu
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
expectedLink: testOpenCloudURL + "/f/spaceid",
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.SpaceUnshared{
|
||||
@@ -261,6 +265,79 @@ https://opencloud.eu
|
||||
ExpiredAt: time.Date(2023, 4, 17, 16, 42, 0, 0, time.UTC),
|
||||
},
|
||||
}),
|
||||
|
||||
Entry("Guest Token Created", testChannel{
|
||||
expectedReceipients: []string{"guest@example.com"},
|
||||
expectedSubject: "Dr. S. Harer shared 'guest folder' with you",
|
||||
expectedTextBody: `Hello,
|
||||
|
||||
Dr. S. Harer has shared "guest folder" with you.
|
||||
|
||||
The link below is personal and can only be used once. Please do not forward it.
|
||||
|
||||
The link is only valid for 30 minutes.
|
||||
|
||||
Click here to view it: https://cloud.example.com/g/v1.sharehash.secret
|
||||
|
||||
|
||||
---
|
||||
OpenCloud - a safe home for all your data
|
||||
https://opencloud.eu
|
||||
`,
|
||||
expectedHTMLBody: `<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<table cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<table cellspacing="0" cellpadding="0" border="0" width="600px">
|
||||
<tr>
|
||||
<td width="20px"> </td>
|
||||
<td style="font-weight:normal; font-size:0.8em; line-height:1.2em; font-family:verdana,'arial',sans;">
|
||||
Hello,
|
||||
<br><br>
|
||||
Dr. S. Harer has shared "guest folder" with you.<br><br>The link below is personal and can only be used once. Please do not forward it.<br><br>The link is only valid for 30 minutes.
|
||||
<br><br>
|
||||
Click here to view it: <a href="https://cloud.example.com/g/v1.sharehash.secret">https://cloud.example.com/g/v1.sharehash.secret</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="20px"> </td>
|
||||
<td style="font-weight:normal; font-size:0.8em; line-height:1.2em; font-family:verdana,'arial',sans;">
|
||||
<footer>
|
||||
<br>
|
||||
<br>
|
||||
--- <br>
|
||||
OpenCloud - a safe home for all your data<br>
|
||||
<a href="https://opencloud.eu">https://opencloud.eu</a>
|
||||
</footer>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
expectedLink: testOpenCloudURL + "/g/v1.sharehash.secret",
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: ocEvents.GuestTokenCreated{
|
||||
Sharer: sharer.GetId(),
|
||||
GranteeEmail: "guest@example.com",
|
||||
ItemID: resourceid,
|
||||
ResourceName: "guest folder",
|
||||
Token: "v1.sharehash.secret",
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -339,7 +416,7 @@ var _ = Describe("Notifications X-Site Scripting", func() {
|
||||
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
|
||||
ch := make(chan events.Event)
|
||||
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "",
|
||||
"", "", "", "", "", "",
|
||||
"", "", "", testOpenCloudURL, "", "",
|
||||
store.Create(), nil, nil)
|
||||
go evts.Run()
|
||||
|
||||
@@ -359,7 +436,7 @@ var _ = Describe("Notifications X-Site Scripting", func() {
|
||||
|
||||
Dr. O'reilly has shared "<script>alert('secrets of the board');</script>" with you.
|
||||
|
||||
Click here to view it: files/shares/with-me
|
||||
Click here to view it: https://cloud.example.com/files/shares/with-me
|
||||
|
||||
|
||||
---
|
||||
@@ -380,7 +457,7 @@ https://opencloud.eu
|
||||
<br><br>
|
||||
Dr. O'reilly has shared "<script>alert('secrets of the board');</script>" with you.
|
||||
<br><br>
|
||||
Click here to view it: <a href="files/shares/with-me">files/shares/with-me</a>
|
||||
Click here to view it: <a href="https://cloud.example.com/files/shares/with-me">https://cloud.example.com/files/shares/with-me</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -410,7 +487,8 @@ https://opencloud.eu
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
|
||||
done: make(chan struct{}),
|
||||
expectedLink: testOpenCloudURL + "/files/shares/with-me",
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.ShareCreated{
|
||||
Sharer: sharer.GetId(),
|
||||
@@ -427,7 +505,7 @@ https://opencloud.eu
|
||||
|
||||
Dr. O'reilly has invited you to join "<script>alert('secret space');</script>".
|
||||
|
||||
Click here to view it: f/spaceid
|
||||
Click here to view it: https://cloud.example.com/f/spaceid
|
||||
|
||||
|
||||
---
|
||||
@@ -449,7 +527,7 @@ https://opencloud.eu
|
||||
<br><br>
|
||||
Dr. O'reilly has invited you to join "<script>alert('secret space');</script>".
|
||||
<br><br>
|
||||
Click here to view it: <a href="f/spaceid">f/spaceid</a>
|
||||
Click here to view it: <a href="https://cloud.example.com/f/spaceid">https://cloud.example.com/f/spaceid</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -477,7 +555,8 @@ https://opencloud.eu
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
done: make(chan struct{}),
|
||||
expectedLink: testOpenCloudURL + "/f/spaceid",
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: events.SpaceShared{
|
||||
Executant: sharer.GetId(),
|
||||
@@ -489,14 +568,18 @@ https://opencloud.eu
|
||||
)
|
||||
})
|
||||
|
||||
// NOTE: This is explictitly not testing the message itself. Should we?
|
||||
const testOpenCloudURL = "https://cloud.example.com"
|
||||
|
||||
type testChannel struct {
|
||||
expectedReceipients []string
|
||||
expectedSubject string
|
||||
expectedTextBody string
|
||||
expectedHTMLBody string
|
||||
expectedSender string
|
||||
done chan struct{}
|
||||
// expectedLink, if set, must be an absolute URL on the OpenCloud instance
|
||||
// and must be linked in the HTML body.
|
||||
expectedLink string
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (tc testChannel) SendMessage(ctx context.Context, m *channels.Message) error {
|
||||
@@ -506,9 +589,15 @@ func (tc testChannel) SendMessage(ctx context.Context, m *channels.Message) erro
|
||||
Expect(tc.expectedSubject).To(Equal(m.Subject))
|
||||
Expect(tc.expectedTextBody).To(Equal(m.TextBody))
|
||||
Expect(tc.expectedSender).To(Equal(m.Sender))
|
||||
Expect(m.HTMLBody).ToNot(BeEmpty())
|
||||
if tc.expectedHTMLBody != "" {
|
||||
Expect(tc.expectedHTMLBody).To(Equal(m.HTMLBody))
|
||||
}
|
||||
if tc.expectedLink != "" {
|
||||
Expect(tc.expectedLink).To(HavePrefix(testOpenCloudURL + "/"))
|
||||
Expect(m.TextBody).To(ContainSubstring(tc.expectedLink))
|
||||
Expect(m.HTMLBody).To(ContainSubstring(`<a href="` + tc.expectedLink + `">`))
|
||||
}
|
||||
tc.done <- struct{}{}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user