Skip to content

Commit

Permalink
feat: add verification function to the notification settings for the …
Browse files Browse the repository at this point in the history
…mailbox (#5464)

#### What type of PR is this?

/kind feature
/area ui
/area core
/milestone 2.14.x

#### What this PR does / why we need it:

为邮件的 `通知设置` 添加验证的功能。

同时为 formkit 增加了一个新的组件 (verificationForm),用于支持验证,它的定义方式如下:
```
- $formkit: verificationForm
  action: "http://localhost:8090/verify/user"
  label: 用户验证
  children:
    - $formkit: text
      label: "用户名"
      name: username
      validation: required
    - $formkit: password
      label: "密码"
      name: password
      validation: required
```

verificationForm 支持 `action` 属性,当前端数据验证通过时,会将其下所包含的子节点数据发送至 action 所代表的接口上。
按上述示例,则验证数据会提交至 `http://localhost:8090/verify/user` 进行验证。验证的数据为 `{name: xxx, password: xxx}`

需要注意的是,verificationForm 只用于包装需要验证的数据,不会破坏原始数据的格式。因此上述数据在提交保存后仍旧为 `{name: xxx, password: xxx}` 而不会变成 `{verificationForm1: {name: xxx, password: xxx}}`

#### How to test it?

1. 测试邮箱中的 `通知设置` 新增的验证按钮是否可以正常验证邮箱。
2. 查看数据是否正常回显

#### Which issue(s) this PR fixes:

Fixes #4714 

#### Does this PR introduce a user-facing change?
```release-note
在邮件通知设置中增加了发送测试的功能。
```
  • Loading branch information
LIlGG committed Mar 26, 2024
1 parent 78c00a2 commit a281015
Show file tree
Hide file tree
Showing 15 changed files with 564 additions and 117 deletions.
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
package run.halo.app.notification;

import com.fasterxml.jackson.databind.JsonNode;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.util.Pair;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.core.extension.notification.Subscription;
import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.notification.EmailSenderHelper.EmailSenderConfig;

/**
* <p>A notifier that can send email.</p>
Expand All @@ -34,7 +32,8 @@ public class EmailNotifier implements ReactiveNotifier {

private final SubscriberEmailResolver subscriberEmailResolver;
private final NotificationTemplateRender notificationTemplateRender;
private final AtomicReference<Pair<EmailSenderConfig, JavaMailSenderImpl>>
private final EmailSenderHelper emailSenderHelper;
private final AtomicReference<Pair<EmailSenderConfig, JavaMailSender>>
emailSenderConfigPairRef = new AtomicReference<>();

@Override
Expand All @@ -48,7 +47,7 @@ public Mono<Void> notify(NotificationContext context) {
return Mono.empty();
}

JavaMailSenderImpl javaMailSender = getJavaMailSender(emailSenderConfig);
JavaMailSender javaMailSender = getJavaMailSender(emailSenderConfig);

String recipient = context.getMessage().getRecipient();
var subscriber = new Subscription.Subscriber();
Expand Down Expand Up @@ -83,55 +82,20 @@ public Mono<Void> notify(NotificationContext context) {
}

@NonNull
private static MimeMessagePreparator getMimeMessagePreparator(String toEmail,
private MimeMessagePreparator getMimeMessagePreparator(String toEmail,
EmailSenderConfig emailSenderConfig, NotificationContext.MessagePayload payload) {
return mimeMessage -> {
MimeMessageHelper helper =
new MimeMessageHelper(mimeMessage, true, StandardCharsets.UTF_8.name());
helper.setFrom(emailSenderConfig.getSender(), emailSenderConfig.getDisplayName());

helper.setSubject(payload.getTitle());
helper.setText(payload.getRawBody(), payload.getHtmlBody());
helper.setTo(toEmail);
};
return emailSenderHelper.createMimeMessagePreparator(emailSenderConfig, toEmail,
payload.getTitle(),
payload.getRawBody(), payload.getHtmlBody());
}

@NonNull
private static JavaMailSenderImpl createJavaMailSender(EmailSenderConfig emailSenderConfig) {
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
javaMailSender.setHost(emailSenderConfig.getHost());
javaMailSender.setPort(emailSenderConfig.getPort());
javaMailSender.setUsername(emailSenderConfig.getUsername());
javaMailSender.setPassword(emailSenderConfig.getPassword());

Properties props = javaMailSender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", "true");
if ("SSL".equals(emailSenderConfig.getEncryption())) {
props.put("mail.smtp.ssl.enable", "true");
}

if ("TLS".equals(emailSenderConfig.getEncryption())) {
props.put("mail.smtp.starttls.enable", "true");
}

if ("NONE".equals(emailSenderConfig.getEncryption())) {
props.put("mail.smtp.ssl.enable", "false");
props.put("mail.smtp.starttls.enable", "false");
}

if (log.isDebugEnabled()) {
props.put("mail.debug", "true");
}
return javaMailSender;
}

JavaMailSenderImpl getJavaMailSender(EmailSenderConfig emailSenderConfig) {
JavaMailSender getJavaMailSender(EmailSenderConfig emailSenderConfig) {
return emailSenderConfigPairRef.updateAndGet(pair -> {
if (pair != null && pair.getFirst().equals(emailSenderConfig)) {
return pair;
}
return Pair.of(emailSenderConfig, createJavaMailSender(emailSenderConfig));
return Pair.of(emailSenderConfig,
emailSenderHelper.createJavaMailSender(emailSenderConfig));
}).getSecond();
}

Expand All @@ -156,34 +120,4 @@ Mono<String> appendHtmlBodyFooter(ReasonAttributes attributes) {
</div>
""", attributes);
}

@Data
static class EmailSenderConfig {
private boolean enable;
private String displayName;
private String username;
private String password;
private String host;
private Integer port;
private String encryption;
private String sender;

/**
* Gets email display name.
*
* @return display name if not blank, otherwise username.
*/
public String getDisplayName() {
return StringUtils.defaultIfBlank(displayName, username);
}

/**
* Gets email sender address.
*
* @return sender if not blank, otherwise username.
*/
public String getSender() {
return StringUtils.defaultIfBlank(sender, username);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package run.halo.app.notification;

import lombok.Data;
import lombok.NonNull;
import org.apache.commons.lang3.StringUtils;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessagePreparator;

public interface EmailSenderHelper {

@NonNull
JavaMailSender createJavaMailSender(EmailSenderConfig senderConfig);

@NonNull
MimeMessagePreparator createMimeMessagePreparator(EmailSenderConfig senderConfig,
String toEmail, String subject, String raw, String html);

@Data
class EmailSenderConfig {
private boolean enable;
private String displayName;
private String username;
private String sender;
private String password;
private String host;
private Integer port;
private String encryption;

/**
* Gets email display name.
*
* @return display name if not blank, otherwise username.
*/
public String getDisplayName() {
return StringUtils.defaultIfBlank(displayName, username);
}

/**
* Gets email sender address.
*
* @return sender if not blank, otherwise username
*/
public String getSender() {
return StringUtils.defaultIfBlank(sender, username);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package run.halo.app.notification;

import java.nio.charset.StandardCharsets;
import java.util.Properties;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.stereotype.Component;

/**
* <p>A default implementation of {@link EmailSenderHelper}.</p>
*
* @author guqing
* @since 2.14.0
*/
@Slf4j
@Component
public class EmailSenderHelperImpl implements EmailSenderHelper {

@Override
@NonNull
public JavaMailSender createJavaMailSender(EmailSenderConfig senderConfig) {
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
javaMailSender.setHost(senderConfig.getHost());
javaMailSender.setPort(senderConfig.getPort());
javaMailSender.setUsername(senderConfig.getUsername());
javaMailSender.setPassword(senderConfig.getPassword());

Properties props = javaMailSender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", "true");
if ("SSL".equals(senderConfig.getEncryption())) {
props.put("mail.smtp.ssl.enable", "true");
}

if ("TLS".equals(senderConfig.getEncryption())) {
props.put("mail.smtp.starttls.enable", "true");
}

if ("NONE".equals(senderConfig.getEncryption())) {
props.put("mail.smtp.ssl.enable", "false");
props.put("mail.smtp.starttls.enable", "false");
}

if (log.isDebugEnabled()) {
props.put("mail.debug", "true");
}
return javaMailSender;
}

@Override
@NonNull
public MimeMessagePreparator createMimeMessagePreparator(EmailSenderConfig senderConfig,
String toEmail, String subject, String raw, String html) {
return mimeMessage -> {
MimeMessageHelper helper =
new MimeMessageHelper(mimeMessage, true, StandardCharsets.UTF_8.name());
helper.setFrom(senderConfig.getSender(), senderConfig.getDisplayName());

helper.setSubject(subject);
helper.setText(raw, html);
helper.setTo(toEmail);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package run.halo.app.notification.endpoint;

import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;

import io.swagger.v3.oas.annotations.media.Schema;
import java.security.Principal;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.mail.MailException;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.User;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.notification.EmailSenderHelper;

/**
* Validation endpoint for email config.
*
* @author guqing
* @since 2.14.0
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class EmailConfigValidationEndpoint implements CustomEndpoint {
private static final String EMAIL_SUBJECT = "测试邮件 - 验证邮箱连通性";
private static final String EMAIL_BODY = """
你好!<br/>
这是一封测试邮件,旨在验证您的邮箱发件配置是否正确。<br/>
此邮件由系统自动发送,请勿回复。<br/>
祝好
""";

private final EmailSenderHelper emailSenderHelper;
private final ReactiveExtensionClient client;

@Override
public RouterFunction<ServerResponse> endpoint() {
var tag = "console.api.notification.halo.run/v1alpha1/Notifier";
return SpringdocRouteBuilder.route()
.POST("/notifiers/default-email-notifier/verify-connection",
this::verifyEmailSenderConfig,
builder -> builder.operationId("VerifyEmailSenderConfig")
.description("Verify email sender config.")
.tag(tag)
.requestBody(requestBodyBuilder()
.required(true)
.implementation(ValidationRequest.class)
)
.response(responseBuilder().implementation(Void.class))
)
.build();
}

private Mono<ServerResponse> verifyEmailSenderConfig(ServerRequest request) {
return request.bodyToMono(ValidationRequest.class)
.switchIfEmpty(
Mono.error(new ServerWebInputException("Required request body is missing."))
)
.flatMap(validationRequest -> getCurrentUserEmail()
.flatMap(recipient -> {
var mailSender = emailSenderHelper.createJavaMailSender(validationRequest);
var message = emailSenderHelper.createMimeMessagePreparator(validationRequest,
recipient, EMAIL_SUBJECT, EMAIL_BODY, EMAIL_BODY);
try {
mailSender.send(message);
} catch (MailException e) {
String errorMsg =
"Failed to send email, please check your email configuration.";
log.error(errorMsg, e);
throw new ServerWebInputException(errorMsg, null, e);
}
return ServerResponse.ok().build();
})
);
}

Mono<String> getCurrentUserEmail() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Principal::getName)
.flatMap(username -> client.fetch(User.class, username))
.flatMap(user -> {
var email = user.getSpec().getEmail();
if (StringUtils.isBlank(email)) {
return Mono.error(new ServerWebInputException(
"Your email is missing, please set it in your profile."));
}
return Mono.just(email);
});
}

@Data
@EqualsAndHashCode(callSuper = true)
@Schema(name = "EmailConfigValidationRequest")
static class ValidationRequest extends EmailSenderHelper.EmailSenderConfig {
}

@Override
public GroupVersion groupVersion() {
return GroupVersion.parseAPIVersion("console.api.notification.halo.run/v1alpha1");
}
}

0 comments on commit a281015

Please sign in to comment.