Merge branch 'chore/sending-email-should-be-nonblocking' into 'release'

Sending emails is now done in a non-blocking way

See merge request theappsmith/internal-tools-server!396
This commit is contained in:
Shrikant Kandula 2020-06-19 07:14:43 +00:00
commit 5ce19962a3
2 changed files with 90 additions and 61 deletions

View File

@ -5,11 +5,12 @@ import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import reactor.core.Exceptions;
import reactor.core.publisher.Mono;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
@ -25,22 +26,41 @@ import java.util.regex.Pattern;
@Slf4j
public class EmailSender {
@Autowired
JavaMailSender emailSender;
final JavaMailSender javaMailSender;
@Autowired
EmailConfig emailConfig;
final EmailConfig emailConfig;
private static final InternetAddress MAIL_FROM = makeFromAddress();
public static final Pattern VALID_EMAIL_ADDRESS_REGEX =
Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE);
public EmailSender(JavaMailSender javaMailSender, EmailConfig emailConfig) {
this.javaMailSender = javaMailSender;
this.emailConfig = emailConfig;
}
private static boolean validateEmail(String emailStr) {
Matcher matcher = VALID_EMAIL_ADDRESS_REGEX.matcher(emailStr);
return matcher.find();
}
public Mono<Void> sendMail(String to, String subject, String text, Map<String, String> params) {
return Mono
.fromSupplier(() -> {
try {
return replaceEmailTemplate(text, params);
} catch (IOException e) {
throw Exceptions.propagate(e);
}
})
.flatMap(emailBody -> sendMail(to, subject, emailBody));
}
public Mono<Void> sendMail(String to, String subject, String text) {
return Mono.fromRunnable(() -> sendMailSync(to, subject, text));
}
/**
* This function sends an HTML email to the user from the default email address
*
@ -48,7 +68,7 @@ public class EmailSender {
* @param subject Subject string.
* @param text HTML Body of the message. This method assumes UTF-8.
*/
public void sendMail(String to, String subject, String text) {
private void sendMailSync(String to, String subject, String text) {
log.debug("Got request to send email to: {} with subject: {}", to, subject);
// Don't send an email for local, dev or test environments
if (!emailConfig.isEmailEnabled()) {
@ -67,7 +87,7 @@ public class EmailSender {
}
log.debug("Going to send email to {} with subject {}", to, subject);
MimeMessage mimeMessage = emailSender.createMimeMessage();
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, "utf-8");
try {
@ -75,7 +95,7 @@ public class EmailSender {
helper.setFrom(MAIL_FROM);
helper.setSubject(subject);
helper.setText(text, true);
emailSender.send(mimeMessage);
javaMailSender.send(mimeMessage);
} catch (MessagingException e) {
log.error("Unable to create the mime message while sending an email to {} with subject: {}. Cause: ", to, subject, e);
} catch (MailException e) {
@ -89,16 +109,15 @@ public class EmailSender {
* @param template The name of the template where the HTML text can be found
* @param params A Map of key-value pairs with the key being the variable in the template & value being the actual
* value with which it must be replaced.
* @return
* @throws IOException
* @return Template string with Mustache replacements applied.
* @throws IOException bubbled from Mustache renderer.
*/
public String replaceEmailTemplate(String template, Map<String, String> params) throws IOException {
private String replaceEmailTemplate(String template, Map<String, String> params) throws IOException {
MustacheFactory mf = new DefaultMustacheFactory();
StringWriter stringWriter = new StringWriter();
Mustache mustache = mf.compile(template);
mustache.execute(stringWriter, params).flush();
String emailTemplate = stringWriter.toString();
return emailTemplate;
return stringWriter.toString();
}
private static InternetAddress makeFromAddress() {

View File

@ -32,11 +32,11 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import reactor.core.Exceptions;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import javax.validation.Validator;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
@ -194,22 +194,28 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
// Save the password reset link and send an email to the user
Mono<Boolean> resetFlowMono = passwordResetTokenMono
.flatMap(resetToken -> passwordResetTokenRepository.save(resetToken))
.map(obj -> {
String resetUrl = String.format(FORGOT_PASSWORD_CLIENT_URL_FORMAT,
.flatMap(passwordResetTokenRepository::save)
.flatMap(obj -> {
String resetUrl = String.format(
FORGOT_PASSWORD_CLIENT_URL_FORMAT,
resetUserPasswordDTO.getBaseUrl(),
URLEncoder.encode(token, StandardCharsets.UTF_8),
URLEncoder.encode(email, StandardCharsets.UTF_8));
URLEncoder.encode(email, StandardCharsets.UTF_8)
);
Map<String, String> params = Map.of("resetUrl", resetUrl);
try {
String emailTemplate = emailSender.replaceEmailTemplate(FORGOT_PASSWORD_EMAIL_TEMPLATE, params);
emailSender.sendMail(email, "Appsmith Password Reset", emailTemplate);
} catch (IOException e) {
log.error("Unable to send email because the template replacement failed. Cause: ", e);
}
return Mono.empty();
return emailSender.sendMail(
email,
"Appsmith Password Reset",
FORGOT_PASSWORD_EMAIL_TEMPLATE,
params
);
})
.thenReturn(true);
.thenReturn(true)
.onErrorResume(error -> {
log.error("Unable to send email because the template replacement failed. Cause: ", error);
return Mono.just(true);
});
// Connect the components to first find a valid user and then initiate the password reset flow
return userMono.then(resetFlowMono);
@ -474,21 +480,25 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
final String finalOriginHeader = originHeader;
return userCreate(user)
.map(savedUser -> sendWelcomeEmail(savedUser, finalOriginHeader));
.flatMap(savedUser -> sendWelcomeEmail(savedUser, finalOriginHeader));
}
public User sendWelcomeEmail(User user, String originHeader) {
try {
Map<String, String> params = new HashMap<>();
params.put("firstName", user.getName());
params.put("appsmithLink", originHeader);
String emailBody = emailSender.replaceEmailTemplate(WELCOME_USER_EMAIL_TEMPLATE, params);
emailSender.sendMail(user.getEmail(), "Welcome to Appsmith", emailBody);
} catch (IOException e) {
// Catching and swallowing this exception because we don't want this to affect the rest of the flow
log.error("Unable to send welcome email to the user {}. Cause: ", user.getEmail(), e);
}
return user;
public Mono<User> sendWelcomeEmail(User user, String originHeader) {
Map<String, String> params = new HashMap<>();
params.put("firstName", user.getName());
params.put("appsmithLink", originHeader);
return emailSender
.sendMail(user.getEmail(), "Welcome to Appsmith", WELCOME_USER_EMAIL_TEMPLATE, params)
.thenReturn(user)
.onErrorResume(error -> {
// Swallowing this exception because we don't want this to affect the rest of the flow.
log.error(
"Ignoring error: Unable to send welcome email to the user {}. Cause: ",
user.getEmail(),
Exceptions.unwrap(error)
);
return Mono.just(user);
});
}
@Override
@ -612,7 +622,7 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
return Mono.zip(organizationWithUserAddedMono, userUpdatedWithOrgMono, currentUserMono)
.map(tuple -> {
.flatMap(tuple -> {
// We reached here. This implies that both user and org got updated without any errors. Proceed forward
// with communication (email) here.
Organization updatedOrg = tuple.getT1();
@ -628,39 +638,39 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
}
params.put("inviter_org_name", updatedOrg.getName());
Mono<Void> emailMono;
if (userExisted.get()) {
// If the user already existed, just send an email informing that the user has been added
// to a new organization
log.debug("Going to send email to user {} informing that the user has been added to new organization {}",
updatedUser.getEmail(), updatedOrg.getName());
try {
String inviteUrl = originHeader;
params.put("inviteUrl", inviteUrl);
String emailBody = emailSender.replaceEmailTemplate(USER_ADDED_TO_ORGANIZATION_EMAIL_TEMPLATE, params);
emailSender.sendMail(updatedUser.getEmail(), "Appsmith: You have been added to a new organization", emailBody);
} catch (IOException e) {
log.error("Unable to send invite user email to {}. Cause: ", updatedUser.getEmail(), e);
}
params.put("inviteUrl", originHeader);
emailMono = emailSender.sendMail(updatedUser.getEmail(), "Appsmith: You have been added to a new organization", USER_ADDED_TO_ORGANIZATION_EMAIL_TEMPLATE, params);
} else {
// The user was created and then added to the organization. Send an email to the user to sign
// up on Appsmith platform with the token generated during create user.
log.debug("Going to send email for invite user to {} with token {}", updatedUser.getEmail(), updatedUser.getInviteToken());
try {
String inviteUrl = String.format(INVITE_USER_CLIENT_URL_FORMAT, originHeader,
URLEncoder.encode(updatedUser.getInviteToken(), StandardCharsets.UTF_8),
URLEncoder.encode(updatedUser.getEmail(), StandardCharsets.UTF_8));
params.put("token", updatedUser.getInviteToken());
params.put("inviteUrl", inviteUrl);
String emailBody = emailSender.replaceEmailTemplate(INVITE_USER_EMAIL_TEMPLATE, params);
emailSender.sendMail(updatedUser.getEmail(), "Invite for Appsmith", emailBody);
} catch (IOException e) {
log.error("Unable to send invite user email to {}. Cause: ", updatedUser.getEmail(), e);
}
String inviteUrl = String.format(
INVITE_USER_CLIENT_URL_FORMAT,
originHeader,
URLEncoder.encode(updatedUser.getInviteToken(), StandardCharsets.UTF_8),
URLEncoder.encode(updatedUser.getEmail(), StandardCharsets.UTF_8)
);
params.put("token", updatedUser.getInviteToken());
params.put("inviteUrl", inviteUrl);
emailMono = emailSender.sendMail(updatedUser.getEmail(), "Invite for Appsmith", INVITE_USER_EMAIL_TEMPLATE, params);
}
// We have sent out the emails. Just send back the saved user.
return updatedUser;
return emailMono
.thenReturn(updatedUser)
.onErrorResume(error -> {
log.error("Unable to send invite user email to {}. Cause: ", updatedUser.getEmail(), error);
return Mono.just(updatedUser);
});
});
}