diff --git a/cmd/layotto/main.go b/cmd/layotto/main.go index 42acd98ade..a27246d87b 100644 --- a/cmd/layotto/main.go +++ b/cmd/layotto/main.go @@ -25,6 +25,8 @@ import ( "mosn.io/layotto/components/cryption" + "mosn.io/layotto/components/email" + "mosn.io/layotto/pkg/grpc/lifecycle" "mosn.io/layotto/components/oss" @@ -43,6 +45,8 @@ import ( aws_cryption "mosn.io/layotto/components/cryption/aws" aliyun_file "mosn.io/layotto/components/file/aliyun" + aliyun_email "mosn.io/layotto/components/email/aliyun" + "github.com/dapr/components-contrib/secretstores" "github.com/dapr/components-contrib/secretstores/aws/parameterstore" "github.com/dapr/components-contrib/secretstores/aws/secretmanager" @@ -311,6 +315,11 @@ func NewRuntimeGrpcServer(data json.RawMessage, opts ...grpc.ServerOption) (mgrp cryption.NewFactory("aliyun.kms", aliyun_cryption.NewCryption), cryption.NewFactory("aws.kms", aws_cryption.NewCryption), ), + + // Email + runtime.WithEmailServiceFactory( + email.NewFactory("aliyun.email", aliyun_email.NewAliyunEmail), + ), // PubSub runtime.WithPubSubFactory( pubsub.NewFactory("redis", func() dapr_comp_pubsub.PubSub { diff --git a/components/email/aliyun/email.go b/components/email/aliyun/email.go new file mode 100644 index 0000000000..8d025235e3 --- /dev/null +++ b/components/email/aliyun/email.go @@ -0,0 +1,146 @@ +/* +* Copyright 2021 Layotto Authors +* +* 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. + */ + +package aliyun + +import ( + "context" + "strings" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + dm20151123 "github.com/alibabacloud-go/dm-20151123/v2/client" + "github.com/alibabacloud-go/tea/tea" + + "mosn.io/layotto/components/email" +) + +type AliyunEmail struct { + client *dm20151123.Client +} + +func NewAliyunEmail() email.EmailService { + return &AliyunEmail{} +} + +var _ email.EmailService = (*AliyunEmail)(nil) + +func (a *AliyunEmail) Init(ctx context.Context, conf *email.Config) error { + accessKeyID := conf.Metadata[email.ClientKey] + accessKeySecret := conf.Metadata[email.ClientSecret] + endpoint := conf.Metadata[email.Endpoint] + config := &openapi.Config{ + // accessKey ID + AccessKeyId: tea.String(accessKeyID), + // accessKey Secret + AccessKeySecret: tea.String(accessKeySecret), + // endpoint, ref https://api.aliyun.com/product/Dm + Endpoint: tea.String(endpoint), + } + + client, err := dm20151123.NewClient(config) + if err != nil { + return err + } + a.client = client + return nil +} + +// SendEmail . +func (a *AliyunEmail) SendEmail(ctx context.Context, req *email.SendEmailRequest) (*email.SendEmailResponse, error) { + if !a.checkSendRequest(req) { + return nil, email.ErrInvalid + } + + // AccountName is the email send from + accountName := req.Address.From + // ToAddress is target addresses the email send to + toAddress := strings.Join(req.Address.To, ",") + + sendMailRequest := &dm20151123.SingleSendMailRequest{} + sendMailRequest. + SetAccountName(accountName). + // AddressType = 1: use the email send from + // ref https://help.aliyun.com/document_detail/29444.html + SetAddressType(1). + // ReplyToAddress = false: the email no need to reply + // ref https://help.aliyun.com/document_detail/29444.html + SetReplyToAddress(false). + SetSubject(req.Subject). + SetToAddress(toAddress). + SetTextBody(req.Content.Text) + + resp, err := a.client.SingleSendMail(sendMailRequest) + if err != nil { + return nil, err + } + return &email.SendEmailResponse{ + RequestId: *resp.Body.RequestId, + }, nil +} + +func (a *AliyunEmail) checkSendRequest(r *email.SendEmailRequest) bool { + // make sure content not empty + if r.Content == nil || r.Content.Text == "" { + return false + } + if info := r.Address; info == nil || info.From == "" || len(info.To) == 0 { + return false + } + return true +} + +// SendEmailWithTemplate . +// template must have been applied in aliyun console, and there need the template name +// receivers must have been filled in aliyun console, and there need the receivers list name +func (a *AliyunEmail) SendEmailWithTemplate(ctx context.Context, req *email.SendEmailWithTemplateRequest) (*email.SendEmailWithTemplateResponse, error) { + if !a.checkSendWithTemplateRequest(req) { + return nil, email.ErrInvalid + } + + // AccountName is the email send from + accountName := req.Address.From + // ReceiversName is the name of the recipient list that is created in advance and uploaded with recipients. + // Only take the element with index zero + receiversName := req.Address.To[0] + + sendMailWithTemplateRequest := &dm20151123.BatchSendMailRequest{} + sendMailWithTemplateRequest. + SetAccountName(accountName). + // AddressType = 1: use the email send from + // ref https://help.aliyun.com/document_detail/29444.html + SetAddressType(1). + SetReceiversName(receiversName). + SetTemplateName(req.Template.TemplateId) + + resp, err := a.client.BatchSendMail(sendMailWithTemplateRequest) + if err != nil { + return nil, err + } + return &email.SendEmailWithTemplateResponse{ + RequestId: *resp.Body.RequestId, + }, nil +} + +func (a *AliyunEmail) checkSendWithTemplateRequest(r *email.SendEmailWithTemplateRequest) bool { + // make sure template exist + if r.Template == nil || r.Template.TemplateId == "" { + return false + } + if info := r.Address; info == nil || info.From == "" || len(info.To) == 0 { + return false + } + return true +} diff --git a/components/email/aliyun/email_test.go b/components/email/aliyun/email_test.go new file mode 100644 index 0000000000..400b9ca162 --- /dev/null +++ b/components/email/aliyun/email_test.go @@ -0,0 +1,88 @@ +/* +* Copyright 2021 Layotto Authors +* +* 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. + */ + +package aliyun + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "mosn.io/layotto/components/email" +) + +func TestInit(t *testing.T) { + a := &AliyunEmail{} + conf := &email.Config{ + Metadata: map[string]string{ + email.ClientKey: "aki", + email.ClientSecret: "aks", + email.Endpoint: "endpoint", + }, + } + err := a.Init(context.TODO(), conf) + assert.Nil(t, err) +} + +func TestSendEmail(t *testing.T) { + a := &AliyunEmail{} + conf := &email.Config{ + Metadata: map[string]string{ + email.ClientKey: "aki", + email.ClientSecret: "aks", + email.Endpoint: "endpoint", + }, + } + err := a.Init(context.TODO(), conf) + assert.Nil(t, err) + + request := &email.SendEmailRequest{ + Subject: "a_subject", + Address: &email.EmailAddress{ + From: "email_send_from", + To: []string{"email_send_to"}, + }, + Content: &email.Content{Text: "some words"}, + } + _, err = a.SendEmail(context.TODO(), request) + assert.Error(t, err) +} + +func TestSendEmailWithTemplate(t *testing.T) { + a := &AliyunEmail{} + conf := &email.Config{ + Metadata: map[string]string{ + email.ClientKey: "aki", + email.ClientSecret: "aks", + email.Endpoint: "endpoint", + }, + } + err := a.Init(context.TODO(), conf) + assert.NoError(t, err) + + request := &email.SendEmailWithTemplateRequest{ + Template: &email.EmailTemplate{ + TemplateId: "a_template", + }, + Address: &email.EmailAddress{ + From: "email_send_from", + To: []string{"receivers_name"}, + }, + } + _, err = a.SendEmailWithTemplate(context.TODO(), request) + assert.Error(t, err) +} diff --git a/components/email/errors.go b/components/email/errors.go new file mode 100644 index 0000000000..326e8d2bbe --- /dev/null +++ b/components/email/errors.go @@ -0,0 +1,23 @@ +/* +* Copyright 2021 Layotto Authors +* +* 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. + */ + +package email + +import "errors" + +var ( + ErrInvalid = errors.New("invalid argument") +) diff --git a/components/email/meta.go b/components/email/meta.go new file mode 100644 index 0000000000..eacdcc42bd --- /dev/null +++ b/components/email/meta.go @@ -0,0 +1,23 @@ +/* +* Copyright 2021 Layotto Authors +* +* 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. + */ + +package email + +const ( + ClientKey = "accessKeyID" + ClientSecret = "accessKeySecret" + Endpoint = "endpoint" +) diff --git a/components/go.mod b/components/go.mod index 7d1638c35a..f4f5cb476d 100644 --- a/components/go.mod +++ b/components/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.4 + github.com/alibabacloud-go/dm-20151123/v2 v2.0.1 github.com/alibabacloud-go/kms-20160120/v3 v3.0.2 github.com/alibabacloud-go/tea v1.1.19 github.com/alicebob/miniredis/v2 v2.16.0 diff --git a/components/go.sum b/components/go.sum index 16416aa1ff..c836b6ba50 100644 --- a/components/go.sum +++ b/components/go.sum @@ -141,11 +141,14 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/alibaba/sentinel-golang v1.0.3/go.mod h1:Lag5rIYyJiPOylK8Kku2P+a23gdKMMqzQS7wTnjWEpk= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.0/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.2/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.4 h1:7Q2FEyqxeZeIkwYMwRC3uphxV4i7O2eV4ETe21d6lS4= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.4/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50= github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/dm-20151123/v2 v2.0.1 h1:wEIEI4vvk7vtiJmXEUnom+ylEBfy107WQjaPNyQc7E4= +github.com/alibabacloud-go/dm-20151123/v2 v2.0.1/go.mod h1:q9cd++SgWfT9U5kdBbRRlvzrr0HOKayLxfe9s0NVZhQ= github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= github.com/alibabacloud-go/kms-20160120/v3 v3.0.2 h1:kxtotOKKdQuO2UlOrOsevBioZfdfV4MYGu1jJ0Rctlk= diff --git a/configs/config_email_aliyun.json b/configs/config_email_aliyun.json new file mode 100644 index 0000000000..51cc8362e0 --- /dev/null +++ b/configs/config_email_aliyun.json @@ -0,0 +1,82 @@ +{ + "servers": [ + { + "default_log_path": "stdout", + "default_log_level": "DEBUG", + "listeners": [ + { + "name": "grpc", + "address": "127.0.0.1:34904", + "bind_port": true, + "filter_chains": [ + { + "filters": [ + { + "type": "grpc", + "config": { + "server_name": "runtime", + "grpc_config": { + "email": { + "email_demo": { + "type": "aliyun.email", + "metadata": { + "accessKeyID": "accessKeyID", + "accessKeySecret": "accessKeySecret", + "endpoint": "endpoint" + } + } + } + } + } + } + ] + } + ] + } + ] + } + ], + "dynamic_resources": { + "lds_config": { + "ads": {}, + "initial_fetch_timeout": "0s", + "resource_api_version": "V3" + }, + "cds_config": { + "ads": {}, + "initial_fetch_timeout": "0s", + "resource_api_version": "V3" + }, + "ads_config": { + "api_type": "GRPC", + "set_node_on_first_message_only": true, + "transport_api_version": "V3", + "grpc_services": [{ + "envoy_grpc": { + "cluster_name": "xds-grpc" + } + }] + } + }, + "static_resources": { + "clusters": [{ + "name": "xds-grpc", + "type": "STATIC", + "connect_timeout": "1s", + "lb_policy": "ROUND_ROBIN", + "load_assignment": { + "cluster_name": "xds-grpc", + "endpoints": [{ + "lb_endpoints": [{ + "endpoint": { + "address": { + "socket_address": {"address": "127.0.0.1", "port_value": 30681} + } + } + } + ] + }] + } + }] + } +} diff --git a/demo/email/client.go b/demo/email/client.go new file mode 100644 index 0000000000..17e6b71a00 --- /dev/null +++ b/demo/email/client.go @@ -0,0 +1,88 @@ +/* +* Copyright 2021 Layotto Authors +* +* 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. + */ + +package main + +import ( + "context" + "fmt" + + "mosn.io/layotto/spec/proto/extension/v1/email" + + "google.golang.org/grpc" +) + +const ( + storeName = "email_demo" +) + +func TestSendEmail() { + // Establish a connection. + conn, err := grpc.Dial("127.0.0.1:34904", grpc.WithInsecure()) + if err != nil { + fmt.Printf("failed to establish connection: %+v", err) + return + } + + // Create a client for the email service. + c := email.NewEmailServiceClient(conn) + + // Create a request to send a common email to specified addresses. + req := &email.SendEmailRequest{ + ComponentName: storeName, + Subject: "email demo", + Content: &email.Content{Text: "hi, this is email demo message"}, + Address: &email.EmailAddress{From: "email_send_from", To: []string{"email_send_to"}}, + } + + // Get response using the client and the request. + resp, err := c.SendEmail(context.Background(), req) + if err != nil { + fmt.Printf("send email request failed: %+v", err) + } + fmt.Printf("send email request success, request id: %s \n", resp.RequestId) +} + +func TestSendEmailWithTemplate() { + // Establish a connection. + conn, err := grpc.Dial("127.0.0.1:34904", grpc.WithInsecure()) + if err != nil { + fmt.Printf("failed to establish connection: %+v", err) + return + } + + // Create a client for the email service. + c := email.NewEmailServiceClient(conn) + + // Create a request to send an email with template to specified receivers_name. + req := &email.SendEmailWithTemplateRequest{ + ComponentName: storeName, + Template: &email.EmailTemplate{TemplateId: "a_template"}, + Address: &email.EmailAddress{From: "email_send_from", To: []string{"receivers_name"}}, + } + + // Get response using the client and the request. + resp, err := c.SendEmailWithTemplate(context.Background(), req) + if err != nil { + fmt.Printf("send email with template request failed: %+v", err) + } + fmt.Printf("send email with template request success, request id: %s \n", resp.RequestId) +} + +func main() { + TestSendEmail() + TestSendEmailWithTemplate() +} diff --git a/go.mod b/go.mod index 903a537d8d..e46a10e642 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( google.golang.org/grpc/examples v0.0.0-20210818220435-8ab16ef276a3 google.golang.org/protobuf v1.28.1 mosn.io/api v1.5.0 - mosn.io/layotto/components v0.0.0-20220413092851-55c58dbb1a23 + mosn.io/layotto/components v0.0.0-20230712052228-c940b7f1367d mosn.io/layotto/spec v0.0.0-20220413092851-55c58dbb1a23 mosn.io/mosn v1.5.1-0.20230529091910-7d48a20e544b mosn.io/pkg v1.5.1-0.20230525074748-e3528eae50d9 @@ -66,6 +66,7 @@ require ( github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.4 // indirect github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect + github.com/alibabacloud-go/dm-20151123/v2 v2.0.1 // indirect github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect github.com/alibabacloud-go/kms-20160120/v3 v3.0.2 // indirect github.com/alibabacloud-go/openapi-util v0.1.0 // indirect diff --git a/go.sum b/go.sum index e87ebfa255..5cc77094cc 100644 --- a/go.sum +++ b/go.sum @@ -151,11 +151,14 @@ github.com/alibaba/sentinel-golang v1.0.3 h1:x/04ZV3ONFsLaNYC/tOEEaZZQIJjhxDSxwZ github.com/alibaba/sentinel-golang v1.0.3/go.mod h1:Lag5rIYyJiPOylK8Kku2P+a23gdKMMqzQS7wTnjWEpk= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.0/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.2/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.4 h1:7Q2FEyqxeZeIkwYMwRC3uphxV4i7O2eV4ETe21d6lS4= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.4/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50= github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/dm-20151123/v2 v2.0.1 h1:wEIEI4vvk7vtiJmXEUnom+ylEBfy107WQjaPNyQc7E4= +github.com/alibabacloud-go/dm-20151123/v2 v2.0.1/go.mod h1:q9cd++SgWfT9U5kdBbRRlvzrr0HOKayLxfe9s0NVZhQ= github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= github.com/alibabacloud-go/kms-20160120/v3 v3.0.2 h1:kxtotOKKdQuO2UlOrOsevBioZfdfV4MYGu1jJ0Rctlk=