From 5f0a3034b818e49ca3946467db35b44e57922ad0 Mon Sep 17 00:00:00 2001 From: Shrikant Kandula Date: Thu, 18 Jun 2020 17:29:36 +0530 Subject: [PATCH] Sending emails is now done in a non-blocking way --- .../server/notifications/EmailSender.java | 45 +++++--- .../server/services/UserServiceImpl.java | 106 ++++++++++-------- 2 files changed, 90 insertions(+), 61 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java index 3942e44486..b41d544f3e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java @@ -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 sendMail(String to, String subject, String text, Map params) { + return Mono + .fromSupplier(() -> { + try { + return replaceEmailTemplate(text, params); + } catch (IOException e) { + throw Exceptions.propagate(e); + } + }) + .flatMap(emailBody -> sendMail(to, subject, emailBody)); + } + + public Mono 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 params) throws IOException { + private String replaceEmailTemplate(String template, Map 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() { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java index 61572338e6..16fb0fb887 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java @@ -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 i // Save the password reset link and send an email to the user Mono 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 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 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 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 sendWelcomeEmail(User user, String originHeader) { + Map 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 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 i } params.put("inviter_org_name", updatedOrg.getName()); + Mono 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); + }); }); }