Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Commit

Permalink
Add http service to send email for shares (cs3org#3304)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmgigi96 authored and vascoguita committed Oct 18, 2022
1 parent 7670123 commit c1f7ee9
Show file tree
Hide file tree
Showing 3 changed files with 382 additions and 0 deletions.
3 changes: 3 additions & 0 deletions changelog/unreleased/mailer-shares.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Enhancement: Add http service to send email for shares

https://github.com/cs3org/reva/pull/3304
1 change: 1 addition & 0 deletions internal/http/services/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
_ "github.com/cs3org/reva/internal/http/services/datagateway"
_ "github.com/cs3org/reva/internal/http/services/dataprovider"
_ "github.com/cs3org/reva/internal/http/services/helloworld"
_ "github.com/cs3org/reva/internal/http/services/mailer"
_ "github.com/cs3org/reva/internal/http/services/mentix"
_ "github.com/cs3org/reva/internal/http/services/meshdirectory"
_ "github.com/cs3org/reva/internal/http/services/metrics"
Expand Down
378 changes: 378 additions & 0 deletions internal/http/services/mailer/mailer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,378 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package mailer

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/smtp"
"os"
"path/filepath"
"strings"
"text/template"

gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
ctxpkg "github.com/cs3org/reva/pkg/ctx"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/pkg/rhttp/global"
"github.com/cs3org/reva/pkg/sharedconf"
"github.com/mitchellh/mapstructure"
"github.com/rs/zerolog"
)

func init() {
global.Register("mailer", New)
}

type config struct {
SMTPAddress string `mapstructure:"smtp_server" docs:";The hostname and port of the SMTP server."`
SenderLogin string `mapstructure:"sender_login" docs:";The email to be used to send mails."`
SenderPassword string `mapstructure:"sender_password" docs:";The sender's password."`
DisableAuth bool `mapstructure:"disable_auth" docs:"false;Whether to disable SMTP auth."`
Prefix string `mapstructure:"prefix"`
BodyTemplatePath string `mapstructure:"body_template_path"`
SubjectTemplate string `mapstructure:"subject_template"`
GatewaySVC string `mapstructure:"gateway_svc"`
}

type svc struct {
conf *config
client gateway.GatewayAPIClient
tplBody *template.Template
tplSubj *template.Template
}

// New creates a new mailer service
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}

conf.init()

client, err := pool.GetGatewayServiceClient(pool.Endpoint(conf.GatewaySVC))
if err != nil {
return nil, err
}

s := &svc{
conf: conf,
client: client,
}

if err = s.initBodyTemplate(); err != nil {
return nil, err
}
if err = s.initSubjectTemplate(); err != nil {
return nil, err
}

return s, nil
}

func (s *svc) Close() error {
return nil
}

func (s *svc) initBodyTemplate() error {
f, err := os.Open(s.conf.BodyTemplatePath)
if err != nil {
return err
}
defer f.Close()

data, err := io.ReadAll(f)
if err != nil {
return err
}

tpl, err := template.New("tpl_body").Parse(string(data))
if err != nil {
return err
}

s.tplBody = tpl
return nil
}

func (s *svc) initSubjectTemplate() error {
tpl, err := template.New("tpl_subj").Parse(s.conf.SubjectTemplate)
if err != nil {
return err
}
s.tplSubj = tpl
return nil
}

func (c *config) init() {
if c.Prefix == "" {
c.Prefix = "mailer"
}

if c.SubjectTemplate == "" {
c.SubjectTemplate = "{{.OwnerName}} ({{.OwnerUsername}}) shared {{if .IsDir}}folder{{else}}file{{end}} '{{.Filename}}' with you"
}

c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC)
}

func (s *svc) Prefix() string {
return s.conf.Prefix
}

func (s *svc) Unprotected() []string {
return nil
}

type out struct {
Recipients []string `json:"recipients"`
}

func getIDsFromRequest(r *http.Request) ([]string, error) {
if err := r.ParseForm(); err != nil {
return nil, err
}

idsSet := make(map[string]struct{})

for _, id := range r.Form["id"] {
if _, ok := idsSet[id]; ok {
continue
}
idsSet[id] = struct{}{}
}

ids := make([]string, 0, len(idsSet))
for id := range idsSet {
ids = append(ids, id)
}

return ids, nil
}

func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}

ctx := r.Context()

ids, err := getIDsFromRequest(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}

if len(ids) == 0 {
http.Error(w, "share id not provided", http.StatusBadRequest)
return
}

var recipients []string
for _, id := range ids {
recipient, err := s.sendMailForShare(ctx, id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
recipients = append(recipients, recipient)
}

w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(out{Recipients: recipients})
})
}

type shareInfo struct {
RecipientEmail string
RecipientUsername string
OwnerEmail string
OwnerName string
OwnerUsername string
ShareType string
Filename string
Path string
IsDir bool
ShareID string
}

func (s *svc) getAuth() smtp.Auth {
if s.conf.DisableAuth {
return nil
}
return smtp.PlainAuth("", s.conf.SenderLogin, s.conf.SenderPassword, strings.SplitN(s.conf.SMTPAddress, ":", 2)[0])
}

func (s *svc) sendMailForShare(ctx context.Context, id string) (string, error) {
share, err := s.getShareInfoByID(ctx, id)
if err != nil {
return "", err
}

msg, err := s.generateMsg(share.OwnerEmail, share.RecipientEmail, share)
if err != nil {
return "", err
}

return share.RecipientEmail, smtp.SendMail(s.conf.SMTPAddress, s.getAuth(), share.OwnerEmail, []string{share.RecipientEmail}, msg)
}

func (s *svc) generateMsg(from, to string, share *shareInfo) ([]byte, error) {
subj, err := s.generateEmailSubject(share)
if err != nil {
return nil, err
}

body, err := s.generateEmailBody(share)
if err != nil {
return nil, err
}

msg := fmt.Sprintf("From: %s\r\n"+
"To: %s\r\n"+
"Subject: %s\r\n\r\n%s\r\n", from, to, subj, body)
return []byte(msg), nil
}

func (s *svc) getShareInfoByID(ctx context.Context, id string) (*shareInfo, error) {
user, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errtypes.UserRequired("user not in context")
}

shareRes, err := s.client.GetShare(ctx, &collaboration.GetShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{
Id: &collaboration.ShareId{
OpaqueId: id,
},
},
},
})

switch {
case err != nil:
return nil, err
case shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
return nil, errtypes.NotFound(fmt.Sprintf("share %s not found", id))
case shareRes.Status.Code != rpc.Code_CODE_OK:
return nil, errtypes.InternalError(shareRes.Status.Message)
}

share := shareRes.Share
statRes, err := s.client.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
ResourceId: share.ResourceId,
},
})

switch {
case err != nil:
return nil, err
case statRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
return nil, errtypes.NotFound("reference not found")
case statRes.Status.Code != rpc.Code_CODE_OK:
return nil, errtypes.InternalError(statRes.Status.Message)
}

file := statRes.Info

info := &shareInfo{}
switch g := share.Grantee.Id.(type) {
case *provider.Grantee_UserId:
grantee, err := s.getUser(ctx, g.UserId)
if err != nil {
return nil, err
}
info.RecipientEmail = grantee.Mail
info.RecipientUsername = grantee.Username
info.ShareType = "user"
case *provider.Grantee_GroupId:
grantee, err := s.getGroup(ctx, g.GroupId)
if err != nil {
return nil, err
}
info.RecipientEmail = grantee.Mail
info.RecipientUsername = grantee.GroupName
info.ShareType = "group"
}

info.OwnerEmail = user.Mail
info.OwnerName = user.DisplayName
info.OwnerUsername = user.Username

info.Path = file.Path
info.Filename = filepath.Base(file.Path)
if file.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
info.IsDir = true
} else {
info.IsDir = false
}

info.ShareID = id

return info, nil
}

func (s *svc) getUser(ctx context.Context, userID *user.UserId) (*user.User, error) {
res, err := s.client.GetUser(ctx, &user.GetUserRequest{
UserId: userID,
})
if err != nil {
return nil, err
}

return res.User, nil
}

func (s *svc) getGroup(ctx context.Context, groupID *group.GroupId) (*group.Group, error) {
res, err := s.client.GetGroup(ctx, &group.GetGroupRequest{
GroupId: groupID,
})
if err != nil {
return nil, err
}

return res.Group, nil
}

func (s *svc) generateEmailSubject(share *shareInfo) (string, error) {
var buf bytes.Buffer
err := s.tplSubj.Execute(&buf, share)
return buf.String(), err
}

func (s *svc) generateEmailBody(share *shareInfo) (string, error) {
var buf bytes.Buffer
err := s.tplBody.Execute(&buf, share)
return buf.String(), err
}

0 comments on commit c1f7ee9

Please sign in to comment.