/* * Copyright (C) 2008-2020, Juick * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.juick; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.juick.model.Message; import com.juick.model.User; import com.juick.www.api.SystemActivity; import jakarta.mail.MessagingException; import jakarta.mail.Multipart; import jakarta.mail.Session; import jakarta.mail.Transport; import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.MimeBodyPart; import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMultipart; import com.juick.util.HttpBadRequestException; import com.juick.www.WebApp; import com.juick.service.EmailService; import com.juick.service.MessagesService; import com.juick.service.UserService; import com.juick.service.component.*; import com.juick.util.MessageUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import javax.annotation.Nonnull; import javax.inject.Inject; import java.util.*; import static com.juick.util.formatters.PlainTextFormatter.formatPost; import static com.juick.util.formatters.PlainTextFormatter.formatUrl; public class EmailManager implements NotificationListener { public static final String MSGID_PATTERN = "\\.|@|<"; private static final Logger logger = LoggerFactory.getLogger(EmailManager.class); @Inject private EmailService emailService; @Inject private MessagesService messagesService; @Inject private UserService userService; @Inject private ObjectMapper jsonMapper; @Inject private WebApp webApp; @Value("${web_domain:localhost}") private String webDomain; @Value("${service_email:}") private String serviceEmail; @Override public void processSystemEvent(@Nonnull SystemEvent systemEvent) { var activity = systemEvent.getActivity(); var msg = activity.getMessage(); var subscribers = activity.getTo(); if (activity.getType().equals(SystemActivity.ActivityType.message)) { processMessage(msg, subscribers); } try { var eventHeader = Collections.singletonMap("X-Event-Version", "1.0"); sendEmail("noreply@juick.com", serviceEmail, "New system event", jsonMapper.writeValueAsString(systemEvent.getActivity()), null, eventHeader); } catch (JsonProcessingException e) { logger.warn("JSON exception", e); } } private void processMessage(Message msg, List<User> subscribedUsers) { if (msg.isService()) { return; } if (MessageUtils.isPM(msg)) { String subject = String.format("Private message from %s", msg.getUser().getName()); emailService.getEmails(msg.getTo().getUid(), true).forEach(email -> emailNotify(email, subject, msg)); } else if (MessageUtils.isReply(msg)) { Message originalMessage = messagesService.getMessage(msg.getMid()).orElseThrow(IllegalStateException::new); String subject = String.format("New reply to %s", originalMessage.getUser().getName()); subscribedUsers.stream() .flatMap(user -> emailService.getEmails(user.getUid(), true).stream()) .forEach(email -> emailNotify(email, subject, msg)); } else { String subject = String.format("New message from %s", msg.getUser().getName()); subscribedUsers .forEach(user -> emailService.getEmails(user.getUid(), true) .forEach(email -> emailNotify(email, subject, msg))); } } @Override public void processPingEvent(PingEvent pingEvent) { } @Async @EventListener public void processMailVerificationEvent(MailVerificationEvent mailVerificationEvent) { if (!sendEmail("noreply@juick.com", mailVerificationEvent.getEmail(), "Juick authorization link", String.format("Follow link to attach this email to Juick account:\n" + "http://juick.com/settings?page=auth-email&code=%s\n\n" + "If you don't know, what this mean - just ignore this mail.\n", mailVerificationEvent.getCode()), StringUtils.EMPTY, Collections.emptyMap())) { throw new HttpBadRequestException(); } } @Async @EventListener public void processAccountVerificationEvent(AccountVerificationEvent accountVerificationEvent) { String signupUrl = String.format("Follow this link to create Juick account: https://juick.com/signup?type=email&hash=%s", accountVerificationEvent.getCode()); sendEmail("noreply@juick.com", accountVerificationEvent.getEmail(), "Juick registration", signupUrl, StringUtils.EMPTY, Collections.emptyMap()); } private void emailNotify(String email, String subject, Message msg) { Map<String, String> headers = new HashMap<>(); if (!MessageUtils.isPM(msg)) { headers.put("Message-ID", String.format("<%d.%d@juick.com>", msg.getMid(), msg.getRid())); } if (MessageUtils.isReply(msg)) { if (msg.getReplyto() > 0) { Message replyto = messagesService.getReply(msg.getMid(), msg.getReplyto()); headers.put("In-Reply-To", String.format("<%d.%d@juick.com>", replyto.getMid(), replyto.getRid())); } else { Message original = messagesService.getMessage(msg.getMid()).orElseThrow(IllegalStateException::new); headers.put("In-Reply-To", String.format("<%d.%d@juick.com>", original.getMid(), original.getRid())); } } String plainText = webApp.renderPlaintext(formatPost(msg, webDomain), formatUrl(msg)).orElseThrow(IllegalStateException::new); String hash = userService.getHashByUID(userService.getUserByEmail(email).getUid()); String htmlText = webApp.renderHtml(MessageUtils.formatHtml(msg), formatUrl(msg), msg, hash).orElseThrow(IllegalStateException::new); sendEmail(StringUtils.EMPTY, email, subject, plainText, htmlText, headers); } public boolean sendEmail(String from, String to, String subject, String textPart, String htmlPart, Map<String, String> messageHeaders) { Properties prop = System.getProperties(); prop.put("mail.smtp.starttls.enable", "true"); Session session = Session.getDefaultInstance(prop); try { Transport transport = session.getTransport("smtp"); MimeMessage message = new MimeMessage(session) { protected void updateMessageID() throws MessagingException { for (Map.Entry<String, String> entry: messageHeaders.entrySet()) { setHeader(entry.getKey(), entry.getValue()); } } }; String fromAddress = StringUtils.isNotEmpty(from) ? from : "juick@juick.com"; message.setFrom(fromAddress); message.addRecipient(jakarta.mail.Message.RecipientType.TO, new InternetAddress(to)); message.setSubject(subject); MimeBodyPart textBodyPart = new MimeBodyPart(); textBodyPart.setContent(textPart, "text/plain; charset=UTF-8"); Multipart multipart = new MimeMultipart("alternative"); multipart.addBodyPart(textBodyPart); if (StringUtils.isNotBlank(htmlPart)) { MimeBodyPart htmlBodyPart = new MimeBodyPart(); htmlBodyPart.setContent(htmlPart, "text/html; charset=UTF-8"); multipart.addBodyPart(htmlBodyPart); } message.setContent(multipart); User emailUser = userService.getUserByEmail(to); if (!emailUser.isAnonymous()) { message.setHeader("List-Id", "<mail-notifications.juick.com>"); message.setHeader("List-Unsubscribe","<mailto:unsubscribe@juick.com>"); message.setHeader("Feedback-ID", String.format("%d:juick", emailUser.getUid())); message.setHeader("Precedence", "bulk"); } message.saveChanges(); transport.connect(); transport.sendMessage(message, message.getAllRecipients()); return true; } catch (MessagingException ex) { logger.error("mail exception", ex); return false; } } }