Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add verification function to the notification settings for the mailbox #5464

Merged
merged 17 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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.getUsername(), emailSenderConfig.getDisplayName());

helper.setSubject(payload.getTitle());
helper.setText(payload.getRawBody(), payload.getHtmlBody());
helper.setTo(toEmail);
};
}

@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;
return emailSenderHelper.createMimeMessagePreparator(emailSenderConfig, toEmail,
payload.getTitle(),
payload.getRawBody(), payload.getHtmlBody());
}

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,24 +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;

/**
* Gets email display name.
*
* @return display name if not blank, otherwise username.
*/
public String getDisplayName() {
return StringUtils.defaultIfBlank(displayName, username);
}
}
}
@@ -0,0 +1,37 @@
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 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);
}
}
}
@@ -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.getUsername(), senderConfig.getDisplayName());

helper.setSubject(subject);
helper.setText(raw, html);
helper.setTo(toEmail);
};
}
}
@@ -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");
}
}