| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 export |
| |
| import ( |
| "crypto/tls" |
| "encoding/json" |
| "fmt" |
| "github.com/apache/answer/pkg/display" |
| "mime" |
| "os" |
| "strings" |
| "time" |
| |
| "github.com/apache/answer/internal/base/constant" |
| "github.com/apache/answer/internal/base/handler" |
| "github.com/apache/answer/internal/base/reason" |
| "github.com/apache/answer/internal/base/translator" |
| "github.com/apache/answer/internal/schema" |
| "github.com/apache/answer/internal/service/config" |
| "github.com/apache/answer/internal/service/siteinfo_common" |
| "github.com/segmentfault/pacman/errors" |
| "github.com/segmentfault/pacman/log" |
| "golang.org/x/net/context" |
| "gopkg.in/gomail.v2" |
| ) |
| |
| // EmailService kit service |
| type EmailService struct { |
| configService *config.ConfigService |
| emailRepo EmailRepo |
| siteInfoService siteinfo_common.SiteInfoCommonService |
| } |
| |
| // EmailRepo email repository |
| type EmailRepo interface { |
| SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error |
| VerifyCode(ctx context.Context, code string) (content string, err error) |
| } |
| |
| // NewEmailService email service |
| func NewEmailService( |
| configService *config.ConfigService, |
| emailRepo EmailRepo, |
| siteInfoService siteinfo_common.SiteInfoCommonService, |
| ) *EmailService { |
| return &EmailService{ |
| configService: configService, |
| emailRepo: emailRepo, |
| siteInfoService: siteInfoService, |
| } |
| } |
| |
| // EmailConfig email config |
| type EmailConfig struct { |
| FromEmail string `json:"from_email"` |
| FromName string `json:"from_name"` |
| SMTPHost string `json:"smtp_host"` |
| SMTPPort int `json:"smtp_port"` |
| Encryption string `json:"encryption"` // "" SSL TLS |
| SMTPUsername string `json:"smtp_username"` |
| SMTPPassword string `json:"smtp_password"` |
| SMTPAuthentication bool `json:"smtp_authentication"` |
| } |
| |
| func (e *EmailConfig) IsSSL() bool { |
| return e.Encryption == "SSL" |
| } |
| |
| func (e *EmailConfig) IsTLS() bool { |
| return e.Encryption == "TLS" |
| } |
| |
| // SaveCode save code |
| func (es *EmailService) SaveCode(ctx context.Context, userID, code, codeContent string) { |
| err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime) |
| if err != nil { |
| log.Error(err) |
| } |
| } |
| |
| // SendAndSaveCode send email and save code |
| func (es *EmailService) SendAndSaveCode(ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string) { |
| err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime) |
| if err != nil { |
| log.Error(err) |
| return |
| } |
| es.Send(ctx, toEmailAddr, subject, body) |
| } |
| |
| // SendAndSaveCodeWithTime send email and save code |
| func (es *EmailService) SendAndSaveCodeWithTime( |
| ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) { |
| err := es.emailRepo.SetCode(ctx, userID, code, codeContent, duration) |
| if err != nil { |
| log.Error(err) |
| return |
| } |
| es.Send(ctx, toEmailAddr, subject, body) |
| } |
| |
| // Send email send |
| func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body string) { |
| log.Infof("try to send email to %s", toEmailAddr) |
| ec, err := es.GetEmailConfig(ctx) |
| if err != nil { |
| log.Errorf("get email config failed: %s", err) |
| return |
| } |
| if len(ec.SMTPHost) == 0 { |
| log.Warnf("smtp host is empty, skip send email") |
| return |
| } |
| |
| m := gomail.NewMessage() |
| fromName := mime.QEncoding.Encode("utf-8", ec.FromName) |
| m.SetHeader("From", fmt.Sprintf("%s <%s>", fromName, ec.FromEmail)) |
| m.SetHeader("To", toEmailAddr) |
| m.SetHeader("Subject", subject) |
| m.SetBody("text/html", body) |
| |
| d := gomail.NewDialer(ec.SMTPHost, ec.SMTPPort, ec.SMTPUsername, ec.SMTPPassword) |
| if ec.IsSSL() { |
| d.SSL = true |
| } |
| if ec.IsTLS() { |
| d.SSL = false |
| } |
| if len(os.Getenv("SKIP_SMTP_TLS_VERIFY")) > 0 { |
| d.TLSConfig = &tls.Config{ServerName: d.Host, InsecureSkipVerify: true} |
| } |
| if err := d.DialAndSend(m); err != nil { |
| log.Errorf("send email to %s failed: %s", toEmailAddr, err) |
| } else { |
| log.Infof("send email to %s success", toEmailAddr) |
| } |
| } |
| |
| // VerifyUrlExpired email send |
| func (es *EmailService) VerifyUrlExpired(ctx context.Context, code string) (content string) { |
| content, err := es.emailRepo.VerifyCode(ctx, code) |
| if err != nil { |
| log.Error(err) |
| } |
| return content |
| } |
| |
| func (es *EmailService) RegisterTemplate(ctx context.Context, registerUrl string) (title, body string, err error) { |
| siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| return |
| } |
| templateData := &schema.RegisterTemplateData{ |
| SiteName: siteInfo.Name, |
| RegisterUrl: registerUrl, |
| } |
| |
| lang := handler.GetLangByCtx(ctx) |
| title = translator.TrWithData(lang, constant.EmailTplKeyRegisterTitle, templateData) |
| body = translator.TrWithData(lang, constant.EmailTplKeyRegisterBody, templateData) |
| return title, body, nil |
| } |
| |
| func (es *EmailService) PassResetTemplate(ctx context.Context, passResetUrl string) (title, body string, err error) { |
| siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| return |
| } |
| |
| templateData := &schema.PassResetTemplateData{SiteName: siteInfo.Name, PassResetUrl: passResetUrl} |
| |
| lang := handler.GetLangByCtx(ctx) |
| title = translator.TrWithData(lang, constant.EmailTplKeyPassResetTitle, templateData) |
| body = translator.TrWithData(lang, constant.EmailTplKeyPassResetBody, templateData) |
| return title, body, nil |
| } |
| |
| func (es *EmailService) ChangeEmailTemplate(ctx context.Context, changeEmailUrl string) (title, body string, err error) { |
| siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| return |
| } |
| templateData := &schema.ChangeEmailTemplateData{ |
| SiteName: siteInfo.Name, |
| ChangeEmailUrl: changeEmailUrl, |
| } |
| |
| lang := handler.GetLangByCtx(ctx) |
| title = translator.TrWithData(lang, constant.EmailTplKeyChangeEmailTitle, templateData) |
| body = translator.TrWithData(lang, constant.EmailTplKeyChangeEmailBody, templateData) |
| return title, body, nil |
| } |
| |
| // TestTemplate send test email template parse |
| func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, err error) { |
| siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| return |
| } |
| templateData := &schema.TestTemplateData{SiteName: siteInfo.Name} |
| |
| lang := handler.GetLangByCtx(ctx) |
| title = translator.TrWithData(lang, constant.EmailTplKeyTestTitle, templateData) |
| body = translator.TrWithData(lang, constant.EmailTplKeyTestBody, templateData) |
| return title, body, nil |
| } |
| |
| // NewAnswerTemplate new answer template |
| func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAnswerTemplateRawData) ( |
| title, body string, err error) { |
| siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| return |
| } |
| seoInfo, err := es.siteInfoService.GetSiteSeo(ctx) |
| if err != nil { |
| return |
| } |
| templateData := &schema.NewAnswerTemplateData{ |
| SiteName: siteInfo.Name, |
| DisplayName: raw.AnswerUserDisplayName, |
| QuestionTitle: raw.QuestionTitle, |
| AnswerUrl: display.AnswerURL(seoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle, raw.AnswerID), |
| AnswerSummary: raw.AnswerSummary, |
| UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), |
| } |
| |
| lang := handler.GetLangByCtx(ctx) |
| title = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerTitle, templateData) |
| body = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerBody, templateData) |
| return title, body, nil |
| } |
| |
| // NewInviteAnswerTemplate new invite answer template |
| func (es *EmailService) NewInviteAnswerTemplate(ctx context.Context, raw *schema.NewInviteAnswerTemplateRawData) ( |
| title, body string, err error) { |
| siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| return |
| } |
| seoInfo, err := es.siteInfoService.GetSiteSeo(ctx) |
| if err != nil { |
| return |
| } |
| templateData := &schema.NewInviteAnswerTemplateData{ |
| SiteName: siteInfo.Name, |
| DisplayName: raw.InviterDisplayName, |
| QuestionTitle: raw.QuestionTitle, |
| InviteUrl: display.QuestionURL(seoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle), |
| UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), |
| } |
| |
| lang := handler.GetLangByCtx(ctx) |
| title = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerTitle, templateData) |
| body = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerBody, templateData) |
| return title, body, nil |
| } |
| |
| // NewCommentTemplate new comment template |
| func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) ( |
| title, body string, err error) { |
| siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| return |
| } |
| seoInfo, err := es.siteInfoService.GetSiteSeo(ctx) |
| if err != nil { |
| return |
| } |
| templateData := &schema.NewCommentTemplateData{ |
| SiteName: siteInfo.Name, |
| DisplayName: raw.CommentUserDisplayName, |
| QuestionTitle: raw.QuestionTitle, |
| CommentSummary: raw.CommentSummary, |
| UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), |
| } |
| templateData.CommentUrl = display.CommentURL(seoInfo.Permalink, |
| siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle, raw.AnswerID, raw.CommentID) |
| |
| lang := handler.GetLangByCtx(ctx) |
| title = translator.TrWithData(lang, constant.EmailTplKeyNewCommentTitle, templateData) |
| body = translator.TrWithData(lang, constant.EmailTplKeyNewCommentBody, templateData) |
| return title, body, nil |
| } |
| |
| // NewQuestionTemplate new question template |
| func (es *EmailService) NewQuestionTemplate(ctx context.Context, raw *schema.NewQuestionTemplateRawData) ( |
| title, body string, err error) { |
| siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) |
| if err != nil { |
| return |
| } |
| seoInfo, err := es.siteInfoService.GetSiteSeo(ctx) |
| if err != nil { |
| return |
| } |
| templateData := &schema.NewQuestionTemplateData{ |
| SiteName: siteInfo.Name, |
| QuestionTitle: raw.QuestionTitle, |
| Tags: strings.Join(raw.Tags, ", "), |
| UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), |
| } |
| templateData.QuestionUrl = display.QuestionURL( |
| seoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle) |
| |
| lang := handler.GetLangByCtx(ctx) |
| title = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionTitle, templateData) |
| body = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionBody, templateData) |
| return title, body, nil |
| } |
| |
| func (es *EmailService) GetEmailConfig(ctx context.Context) (ec *EmailConfig, err error) { |
| emailConf, err := es.configService.GetStringValue(ctx, constant.EmailConfigKey) |
| if err != nil { |
| return nil, err |
| } |
| ec = &EmailConfig{} |
| err = json.Unmarshal([]byte(emailConf), ec) |
| if err != nil { |
| log.Errorf("old email config format is invalid, you need to update smtp config: %v", err) |
| return nil, errors.BadRequest(reason.SiteInfoConfigNotFound) |
| } |
| return ec, nil |
| } |
| |
| // SetEmailConfig set email config |
| func (es *EmailService) SetEmailConfig(ctx context.Context, ec *EmailConfig) (err error) { |
| data, _ := json.Marshal(ec) |
| return es.configService.UpdateConfig(ctx, constant.EmailConfigKey, string(data)) |
| } |