Refactor email service and remove oh-my-email dependency (#584)

* Refactor email service

* Remove oh-my-email dependency

* Add mail dependency

* Provide connection test with email server

* Enable send message asynchronously

* Make more friendly information.
pull/587/head
John Niang 2020-02-21 21:21:28 +08:00 committed by GitHub
parent 4092ed1633
commit b7b11e2c8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 510 additions and 148 deletions

View File

@ -42,7 +42,6 @@ bootJar {
}
ext {
ohMyEmailVersion = '0.0.4'
hutoolVersion = '5.0.3'
upyunSdkVersion = '4.0.1'
qiniuSdkVersion = '7.2.18'
@ -72,8 +71,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-undertow'
implementation 'org.springframework.boot:spring-boot-starter-freemarker'
implementation "kr.pe.kwonnam.freemarker:freemarker-template-inheritance:$templateInheritance"
implementation "com.sun.mail:jakarta.mail"
implementation "io.github.biezhi:oh-my-email:$ohMyEmailVersion"
implementation "cn.hutool:hutool-core:$hutoolVersion"
implementation "cn.hutool:hutool-crypto:$hutoolVersion"
implementation "cn.hutool:hutool-extra:$hutoolVersion"
@ -110,7 +109,7 @@ dependencies {
implementation "org.json:json:$jsonVersion"
implementation "com.alibaba:fastjson:$fastJsonVersion"
implementation "org.iq80.leveldb:leveldb:$levelDbVersion"
runtimeOnly "com.h2database:h2:$h2Version"
runtimeOnly 'mysql:mysql-connector-java'

View File

@ -5,9 +5,9 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import run.halo.app.mail.MailService;
import run.halo.app.model.params.MailParam;
import run.halo.app.model.support.BaseResponse;
import run.halo.app.service.MailService;
import javax.validation.Valid;
@ -29,8 +29,16 @@ public class MailController {
@PostMapping("test")
@ApiOperation("Tests the SMTP service")
public BaseResponse testMail(@Valid @RequestBody MailParam mailParam) {
mailService.sendMail(mailParam.getTo(), mailParam.getSubject(), mailParam.getContent());
return BaseResponse.ok("发送成功");
public BaseResponse<String> testMail(@Valid @RequestBody MailParam mailParam) {
mailService.sendTextMail(mailParam.getTo(), mailParam.getSubject(), mailParam.getContent());
return BaseResponse.ok("已发送,请查收。若确认没有收到邮件,请检查服务器日志");
}
@PostMapping("test/connection")
@ApiOperation("Test connection with email server")
public BaseResponse<String> testConnection() {
mailService.testConnection();
return BaseResponse.ok("您和邮箱服务器的连接通畅");
}
}

View File

@ -10,6 +10,7 @@ import org.springframework.stereotype.Component;
import run.halo.app.event.comment.CommentNewEvent;
import run.halo.app.event.comment.CommentReplyEvent;
import run.halo.app.exception.ServiceException;
import run.halo.app.mail.MailService;
import run.halo.app.model.entity.*;
import run.halo.app.model.properties.CommentProperties;
import run.halo.app.service.*;
@ -141,7 +142,7 @@ public class CommentEventListener {
/**
* Received a new reply comment event.
*
* @param newEvent reply comment event.
* @param replyEvent reply comment event.
*/
@Async
@EventListener

View File

@ -0,0 +1,241 @@
package run.halo.app.mail;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.util.Assert;
import run.halo.app.exception.EmailException;
import run.halo.app.model.properties.EmailProperties;
import run.halo.app.service.OptionService;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Abstract mail service.
*
* @author johnniang
*/
@Slf4j
public abstract class AbstractMailService implements MailService {
private static final int DEFAULT_POOL_SIZE = 5;
private JavaMailSender cachedMailSender;
private MailProperties cachedMailProperties;
private String cachedFromName;
protected final OptionService optionService;
@Nullable
private ExecutorService executorService;
protected AbstractMailService(OptionService optionService) {
this.optionService = optionService;
}
public void setExecutorService(ExecutorService executorService) {
this.executorService = executorService;
}
@NonNull
public ExecutorService getExecutorService() {
if (this.executorService == null) {
this.executorService = Executors.newFixedThreadPool(DEFAULT_POOL_SIZE);
}
return executorService;
}
/**
* Test connection with email server.
*/
public void testConnection() {
JavaMailSender javaMailSender = getMailSender();
if (javaMailSender instanceof JavaMailSenderImpl) {
JavaMailSenderImpl mailSender = (JavaMailSenderImpl) javaMailSender;
try {
mailSender.testConnection();
} catch (MessagingException e) {
throw new EmailException("无法连接到邮箱服务器,请检查邮箱配置.[" + e.getMessage() + "]", e);
}
}
}
/**
* Send mail template.
*
* @param callback mime message callback.
*/
protected void sendMailTemplate(@Nullable Callback callback) {
if (callback == null) {
log.info("Callback is null, skip to send email");
return;
}
// check if mail is enable
Boolean emailEnabled = optionService.getByPropertyOrDefault(EmailProperties.ENABLED, Boolean.class);
if (!emailEnabled) {
// If disabled
log.info("Email has been disabled by yourself, you can re-enable it through email settings on admin page.");
return;
}
// get mail sender
JavaMailSender mailSender = getMailSender();
printMailConfig();
// create mime message helper
MimeMessageHelper messageHelper = new MimeMessageHelper(mailSender.createMimeMessage());
try {
// set from-name
messageHelper.setFrom(getFromAddress(mailSender));
// handle message set separately
callback.handle(messageHelper);
// get mime message
MimeMessage mimeMessage = messageHelper.getMimeMessage();
// send email
mailSender.send(mimeMessage);
log.info("Sent an email to [{}] successfully, subject: [{}], sent date: [{}]",
Arrays.toString(mimeMessage.getAllRecipients()),
mimeMessage.getSubject(),
mimeMessage.getSentDate());
} catch (Exception e) {
throw new EmailException("邮件发送失败,请检查 SMTP 服务配置是否正确", e);
}
}
/**
* Send mail template if executor service is enable.
*
* @param callback callback message handler
* @param tryToAsync if the send procedure should try to asynchronous
*/
protected void sendMailTemplate(boolean tryToAsync, @Nullable Callback callback) {
ExecutorService executorService = getExecutorService();
if (tryToAsync && executorService != null) {
// send mail asynchronously
executorService.execute(() -> sendMailTemplate(callback));
} else {
// send mail synchronously
sendMailTemplate(callback);
}
}
/**
* Get java mail sender.
*
* @return java mail sender
*/
@NonNull
private synchronized JavaMailSender getMailSender() {
if (this.cachedMailSender == null) {
// create mail sender factory
MailSenderFactory mailSenderFactory = new MailSenderFactory();
// get mail sender
this.cachedMailSender = mailSenderFactory.getMailSender(getMailProperties());
}
return this.cachedMailSender;
}
/**
* Get from-address.
*
* @param javaMailSender java mail sender.
* @return from-name internet address
* @throws UnsupportedEncodingException throws when you give a wrong character encoding
*/
private synchronized InternetAddress getFromAddress(@NonNull JavaMailSender javaMailSender) throws UnsupportedEncodingException {
Assert.notNull(javaMailSender, "Java mail sender must not be null");
if (StringUtils.isBlank(this.cachedFromName)) {
// set personal name
this.cachedFromName = optionService.getByPropertyOfNonNull(EmailProperties.FROM_NAME).toString();
}
if (javaMailSender instanceof JavaMailSenderImpl) {
// get user name(email)
JavaMailSenderImpl mailSender = (JavaMailSenderImpl) javaMailSender;
String username = mailSender.getUsername();
// build internet address
return new InternetAddress(username, this.cachedFromName, mailSender.getDefaultEncoding());
}
throw new UnsupportedOperationException("Unsupported java mail sender: " + javaMailSender.getClass().getName());
}
/**
* Get mail properties.
*
* @return mail properties
*/
@NonNull
private synchronized MailProperties getMailProperties() {
if (cachedMailProperties == null) {
// create mail properties
MailProperties mailProperties = new MailProperties(log.isDebugEnabled());
// set properties
mailProperties.setHost(optionService.getByPropertyOrDefault(EmailProperties.HOST, String.class));
mailProperties.setPort(optionService.getByPropertyOrDefault(EmailProperties.SSL_PORT, Integer.class));
mailProperties.setUsername(optionService.getByPropertyOrDefault(EmailProperties.USERNAME, String.class));
mailProperties.setPassword(optionService.getByPropertyOrDefault(EmailProperties.PASSWORD, String.class));
mailProperties.setProtocol(optionService.getByPropertyOrDefault(EmailProperties.PROTOCOL, String.class));
this.cachedMailProperties = mailProperties;
}
return this.cachedMailProperties;
}
/**
* Print mail configuration.
*/
private void printMailConfig() {
if (!log.isDebugEnabled()) {
return;
}
// get mail properties
MailProperties mailProperties = getMailProperties();
log.debug(mailProperties.toString());
}
/**
* Clear cached instance.
*/
protected void clearCache() {
this.cachedMailSender = null;
this.cachedFromName = null;
this.cachedMailProperties = null;
log.debug("Cleared all mail caches");
}
/**
* Message callback.
*/
protected interface Callback {
/**
* Handle message set.
*
* @param messageHelper mime message helper
* @throws Exception if something goes wrong
*/
void handle(@NonNull MimeMessageHelper messageHelper) throws Exception;
}
}

View File

@ -0,0 +1,61 @@
package run.halo.app.mail;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* Mail properties.
*
* @author johnniang
*/
public class MailProperties extends org.springframework.boot.autoconfigure.mail.MailProperties {
private Map<String, String> properties;
public MailProperties() {
this(false);
}
public MailProperties(boolean needDebug) {
// set some default properties
addProperties("mail.debug", Boolean.toString(needDebug));
addProperties("mail.smtp.auth", Boolean.TRUE.toString());
addProperties("mail.smtp.ssl.enable", Boolean.TRUE.toString());
addProperties("mail.smtp.timeout", "10000");
}
public void addProperties(String key, String value) {
if (properties == null) {
properties = new HashMap<>();
}
properties.put(key, value);
}
public void setProperties(Map<String, String> properties) {
this.properties = properties;
}
@Override
public Map<String, String> getProperties() {
return this.properties;
}
@Override
public String toString() {
final String lineSuffix = ",\n";
final StringBuffer sb = new StringBuffer();
sb.append("MailProperties{").append(lineSuffix);
sb.append("host=").append(getHost()).append(lineSuffix);
sb.append("username=").append(getUsername()).append(lineSuffix);
sb.append("password=").append(StringUtils.isBlank(getPassword()) ? "<null>" : "<non-null>");
sb.append("port=").append(getPort()).append(lineSuffix);
sb.append("protocol=").append(getProtocol()).append(lineSuffix);
sb.append("defaultEncoding=").append(getDefaultEncoding()).append(lineSuffix);
sb.append("properties=").append(properties).append(lineSuffix);
sb.append('}');
return sb.toString();
}
}

View File

@ -0,0 +1,51 @@
package run.halo.app.mail;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.lang.NonNull;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import java.util.Properties;
/**
* Java mail sender factory.
*
* @author johnniang
*/
public class MailSenderFactory {
/**
* Get mail sender.
*
* @param mailProperties mail properties must not be null
* @return java mail sender
*/
@NonNull
public JavaMailSender getMailSender(@NonNull MailProperties mailProperties) {
Assert.notNull(mailProperties, "Mail properties must not be null");
// create mail sender
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
// set properties
setProperties(mailSender, mailProperties);
return mailSender;
}
private void setProperties(@NonNull JavaMailSenderImpl mailSender, @NonNull MailProperties mailProperties) {
mailSender.setHost(mailProperties.getHost());
mailSender.setPort(mailProperties.getPort());
mailSender.setUsername(mailProperties.getUsername());
mailSender.setPassword(mailProperties.getPassword());
mailSender.setProtocol(mailProperties.getProtocol());
mailSender.setDefaultEncoding(mailProperties.getDefaultEncoding().name());
if (!CollectionUtils.isEmpty(mailProperties.getProperties())) {
Properties properties = new Properties();
properties.putAll(mailProperties.getProperties());
mailSender.setJavaMailProperties(properties);
}
}
}

View File

@ -1,4 +1,4 @@
package run.halo.app.service;
package run.halo.app.mail;
import java.util.Map;
@ -17,7 +17,7 @@ public interface MailService {
* @param subject subject
* @param content content
*/
void sendMail(String to, String subject, String content);
void sendTextMail(String to, String subject, String content);
/**
* Send a email with html
@ -36,7 +36,12 @@ public interface MailService {
* @param subject subject
* @param content content
* @param templateName template name
* @param attachFilename attachment path
* @param attachFilePath attachment full path name
*/
void sendAttachMail(String to, String subject, Map<String, Object> content, String templateName, String attachFilename);
void sendAttachMail(String to, String subject, Map<String, Object> content, String templateName, String attachFilePath);
/**
* Test email server connection.
*/
void testConnection();
}

View File

@ -0,0 +1,78 @@
package run.halo.app.mail;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import run.halo.app.event.options.OptionUpdatedEvent;
import run.halo.app.service.OptionService;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
/**
* Mail service implementation.
*
* @author ryanwang
* @author johnniang
* @date 2019-03-17
*/
@Slf4j
@Service
public class MailServiceImpl extends AbstractMailService implements ApplicationListener<OptionUpdatedEvent> {
private final FreeMarkerConfigurer freeMarker;
public MailServiceImpl(FreeMarkerConfigurer freeMarker,
OptionService optionService) {
super(optionService);
this.freeMarker = freeMarker;
}
@Override
public void sendTextMail(String to, String subject, String content) {
sendMailTemplate(true, messageHelper -> {
messageHelper.setSubject(subject);
messageHelper.setTo(to);
messageHelper.setText(content);
});
}
@Override
public void sendTemplateMail(String to, String subject, Map<String, Object> content, String templateName) {
sendMailTemplate(true, messageHelper -> {
// build message content with freemarker
Template template = freeMarker.getConfiguration().getTemplate(templateName);
String contentResult = FreeMarkerTemplateUtils.processTemplateIntoString(template, content);
messageHelper.setSubject(subject);
messageHelper.setTo(to);
messageHelper.setText(contentResult, true);
});
}
@Override
public void sendAttachMail(String to, String subject, Map<String, Object> content, String templateName, String attachFilePath) {
sendMailTemplate(true, (messageHelper) -> {
messageHelper.setSubject(subject);
messageHelper.setTo(to);
Path attachmentPath = Paths.get(attachFilePath);
messageHelper.addAttachment(attachmentPath.getFileName().toString(), attachmentPath.toFile());
});
}
@Override
public void testConnection() {
super.testConnection();
}
@Override
public void onApplicationEvent(OptionUpdatedEvent event) {
// clear the cached java mail sender
clearCache();
}
}

View File

@ -19,6 +19,41 @@ import java.util.Map;
*/
public interface PropertyEnum extends ValueEnum<String> {
/**
* Get property type.
*
* @return property type
*/
Class<?> getType();
/**
* Default value.
*
* @return default value
*/
@Nullable
String defaultValue();
/**
* Default value with given type.
*
* @param propertyType property type must not be null
* @param <T> property type
* @return default value with given type
*/
@Nullable
default <T> T defaultValue(Class<T> propertyType) {
// Get default value
String defaultValue = defaultValue();
if (defaultValue == null) {
return null;
}
// Convert to the given type
return PropertyEnum.convertTo(defaultValue, propertyType);
}
/**
* Converts to value with corresponding type
*
@ -173,17 +208,4 @@ public interface PropertyEnum extends ValueEnum<String> {
return result;
}
/**
* Get property type.
*
* @return property type
*/
Class<?> getType();
/**
* Default value.
*
* @return default value
*/
String defaultValue();
}

View File

@ -196,6 +196,18 @@ public interface OptionService extends CrudService<Option, Integer> {
*/
<T> T getByPropertyOrDefault(@NonNull PropertyEnum property, @NonNull Class<T> propertyType, T defaultValue);
/**
* Gets property value by blog property.
* <p>
* Default value from property default value.
*
* @param property blog property must not be null
* @param propertyType property type must not be null
* @param <T> property type
* @return property value
*/
<T> T getByPropertyOrDefault(@NonNull PropertyEnum property, @NonNull Class<T> propertyType);
/**
* Gets property value by blog property.
*

View File

@ -18,6 +18,7 @@ import run.halo.app.event.logger.LogEvent;
import run.halo.app.exception.BadRequestException;
import run.halo.app.exception.NotFoundException;
import run.halo.app.exception.ServiceException;
import run.halo.app.mail.MailService;
import run.halo.app.model.dto.EnvironmentDTO;
import run.halo.app.model.dto.StatisticDTO;
import run.halo.app.model.entity.User;
@ -228,7 +229,7 @@ public class AdminServiceImpl implements AdminService {
// Send email to administrator.
String content = "您正在进行密码重置操作,如不是本人操作,请尽快做好相应措施。密码重置验证码如下(五分钟有效):\n" + code;
mailService.sendMail(param.getEmail(), "找回密码验证码", content);
mailService.sendTextMail(param.getEmail(), "找回密码验证码", content);
}
@Override

View File

@ -1,122 +0,0 @@
package run.halo.app.service.impl;
import cn.hutool.core.text.StrBuilder;
import freemarker.template.Template;
import io.github.biezhi.ome.OhMyEmail;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import run.halo.app.exception.EmailException;
import run.halo.app.model.properties.EmailProperties;
import run.halo.app.service.MailService;
import run.halo.app.service.OptionService;
import java.io.File;
import java.util.Map;
import java.util.Properties;
/**
* Mail service implementation.
*
* @author ryanwang
* @date 2019-03-17
*/
@Slf4j
@Service
public class MailServiceImpl implements MailService {
private final FreeMarkerConfigurer freeMarker;
private final OptionService optionService;
public MailServiceImpl(FreeMarkerConfigurer freeMarker,
OptionService optionService) {
this.freeMarker = freeMarker;
this.optionService = optionService;
}
@Override
public void sendMail(String to, String subject, String content) {
loadConfig();
String fromUsername = optionService.getByPropertyOfNonNull(EmailProperties.FROM_NAME).toString();
try {
OhMyEmail.subject(subject)
.from(fromUsername)
.to(to)
.text(content)
.send();
} catch (Exception e) {
log.debug("Email properties: to username: [{}], from username: [{}], subject: [{}], content: [{}]",
to, fromUsername, subject, content);
throw new EmailException("发送邮件到 " + to + " 失败,请检查 SMTP 服务配置是否正确", e);
}
}
@Override
public void sendTemplateMail(String to, String subject, Map<String, Object> content, String templateName) {
loadConfig();
String fromUsername = optionService.getByPropertyOfNonNull(EmailProperties.FROM_NAME).toString();
try {
StrBuilder text = new StrBuilder();
Template template = freeMarker.getConfiguration().getTemplate(templateName);
text.append(FreeMarkerTemplateUtils.processTemplateIntoString(template, content));
OhMyEmail.subject(subject)
.from(fromUsername)
.to(to)
.html(text.toString())
.send();
} catch (Exception e) {
log.debug("Email properties: to username: [{}], from username: [{}], subject: [{}], template name: [{}], content: [{}]",
to, fromUsername, subject, templateName, content);
throw new EmailException("发送模板邮件到 " + to + " 失败,请检查 SMTP 服务配置是否正确", e);
}
}
@Override
public void sendAttachMail(String to, String subject, Map<String, Object> content, String templateName, String attachFilename) {
loadConfig();
String fromUsername = optionService.getByPropertyOfNonNull(EmailProperties.FROM_NAME).toString();
File file = new File(attachFilename);
try {
Template template = freeMarker.getConfiguration().getTemplate(templateName);
OhMyEmail.subject(subject)
.from(fromUsername)
.to(to)
.html(FreeMarkerTemplateUtils.processTemplateIntoString(template, content))
.attach(file, file.getName())
.send();
} catch (Exception e) {
log.debug("Email properties: to username: [{}], from username: [{}], subject: [{}], template name: [{}], attachment: [{}], content: [{}]",
to, fromUsername, subject, templateName, attachFilename, content);
throw new EmailException("发送附件邮件到 " + to + " 失败,请检查 SMTP 服务配置是否正确", e);
}
}
/**
* Loads email config.
*/
private void loadConfig() {
// Get default properties
Properties defaultProperties = OhMyEmail.defaultConfig(log.isDebugEnabled());
// Set smtp host
defaultProperties.setProperty("mail.smtp.host", optionService.getByPropertyOfNonNull(EmailProperties.HOST).toString());
defaultProperties.setProperty("mail.transport.protocol", optionService.getByPropertyOrDefault(EmailProperties.PROTOCOL, String.class, EmailProperties.PROTOCOL.defaultValue()));
defaultProperties.setProperty("mail.smtp.port", optionService.getByPropertyOrDefault(EmailProperties.SSL_PORT, String.class, EmailProperties.SSL_PORT.defaultValue()));
// Config email
OhMyEmail.config(defaultProperties,
optionService.getByPropertyOfNonNull(EmailProperties.USERNAME).toString(),
optionService.getByPropertyOfNonNull(EmailProperties.PASSWORD).toString());
}
}

View File

@ -322,6 +322,11 @@ public class OptionServiceImpl extends AbstractCrudService<Option, Integer> impl
return getByProperty(property, propertyType).orElse(defaultValue);
}
@Override
public <T> T getByPropertyOrDefault(PropertyEnum property, Class<T> propertyType) {
return getByProperty(property, propertyType).orElse(property.defaultValue(propertyType));
}
@Override
public <T> Optional<T> getByProperty(PropertyEnum property, Class<T> propertyType) {
return getByProperty(property).map(propertyValue -> PropertyEnum.convertTo(propertyValue.toString(), propertyType));