/* * matrix-appservice-email - Matrix Bridge to E-mail * Copyright (C) 2017 Kamax Sarl * * https://www.kamax.io/ * * 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 io.kamax.matrix.bridge.email.model.email; import io.kamax.matrix.MatrixID; import io.kamax.matrix._MatrixContent; import io.kamax.matrix._MatrixUser; import io.kamax.matrix.bridge.email.config.email.EmailReceiverConfig; import io.kamax.matrix.bridge.email.config.email.EmailSenderConfig; import io.kamax.matrix.bridge.email.model.BridgeMessageContent; import io.kamax.matrix.bridge.email.model.BridgeMessageHtmlContent; import io.kamax.matrix.bridge.email.model._BridgeMessageContent; import io.kamax.matrix.bridge.email.model.matrix._MatrixBridgeMessage; import io.kamax.matrix.bridge.email.model.subscription.SubscriptionEvents; import io.kamax.matrix.bridge.email.model.subscription.SubscriptionPortalService; import io.kamax.matrix.bridge.email.model.subscription._BridgeSubscription; import io.kamax.matrix.bridge.email.model.subscription._SubscriptionEvent; import io.kamax.matrix.client._MatrixClient; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.MimeTypeUtils; import javax.mail.MessagingException; import javax.mail.Session; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; @Component public class EmailFormatterOutboud implements InitializingBean, _EmailFormatterOutbound { private Logger log = LoggerFactory.getLogger(EmailFormatterOutboud.class); @Autowired private EmailSenderConfig sendCfg; @Autowired private EmailReceiverConfig recvCfg; @Autowired private SubscriptionPortalService portalSvc; @Autowired private _EmailTemplateManager templateMgr; private Session session = Session.getInstance(System.getProperties()); private DateTimeFormatter hourFormatter = DateTimeFormatter.ofPattern("HH"); private DateTimeFormatter minFormatter = DateTimeFormatter.ofPattern("mm"); private DateTimeFormatter secFormatter = DateTimeFormatter.ofPattern("ss"); private String senderAvatarId = "sender.avatar@matrix"; @Override public void afterPropertiesSet() throws Exception { if (!templateMgr.get(SubscriptionEvents.OnMessage).isPresent()) { log.error("Configuration error: template list for onMessage notification event cannot be empty"); System.exit(1); } } private String getSubscriptionManageLink(String token) { return portalSvc.getPublicLink(token); } private String getHtml(String text) { text = text.replaceAll("\r\n", "<br/>").replaceAll("\n", "<br/>"); return "<span>" + text + "</span>"; } private String processToken(TokenData data, String template) { template = StringUtils.replace(template, EmailTemplateToken.ManageUrl.getToken(), data.getManageUrl()); template = StringUtils.replace(template, EmailTemplateToken.MsgTimeHour.getToken(), data.getTimeHour()); template = StringUtils.replace(template, EmailTemplateToken.MsgTimeMin.getToken(), data.getTimeMin()); template = StringUtils.replace(template, EmailTemplateToken.MsgTimeSec.getToken(), data.getTimeSec()); template = StringUtils.replace(template, EmailTemplateToken.ReceiverAddress.getToken(), data.getReceiverAddress()); template = StringUtils.replace(template, EmailTemplateToken.SenderAddress.getToken(), data.getSenderAddress()); template = StringUtils.replace(template, EmailTemplateToken.SenderName.getToken(), data.getSenderName()); template = StringUtils.replace(template, EmailTemplateToken.SenderAvatar.getToken(), senderAvatarId); template = StringUtils.replace(template, EmailTemplateToken.Sender.getToken(), data.getSender()); template = StringUtils.replace(template, EmailTemplateToken.RoomAddress.getToken(), data.getRoomAddress()); template = StringUtils.replace(template, EmailTemplateToken.RoomName.getToken(), data.getRoomName()); template = StringUtils.replace(template, EmailTemplateToken.Room.getToken(), data.getRoom()); return template; } private String processToken(TokenData data, String template, String content) { return StringUtils.replace(processToken(data, template), EmailTemplateToken.MsgContent.getToken(), content); } private MimeBodyPart makeBodyPart(TokenData data, _EmailTemplateContent template, _BridgeMessageContent content) throws IOException, MessagingException { StringBuilder partRaw = new StringBuilder(); String header = processToken(data, template.getHeader()); String footer = processToken(data, template.getFooter()); String contentString = processToken(data, template.getContent(), content.getContentAsString()); partRaw.append(header).append(contentString).append(footer); MimeBodyPart part = new MimeBodyPart(); part.setText(partRaw.toString(), StandardCharsets.UTF_8.name(), template.getType().replace("text/", "")); log.info("Created body part of type {}", template.getType()); return part; } private MimeMessage makeEmail(TokenData data, _EmailTemplate template, MimeMultipart body, boolean allowReply) throws MessagingException, UnsupportedEncodingException { MimeMessage msg = new MimeMessage(session); if (allowReply) { msg.setReplyTo(InternetAddress.parse(recvCfg.getEmail().replace("%KEY%", data.getKey()))); } String sender = data.isSelf() ? sendCfg.getName() : data.getSenderName(); msg.setFrom(new InternetAddress(sendCfg.getEmail(), sender, StandardCharsets.UTF_8.name())); msg.setSubject(processToken(data, template.getSubject())); msg.setContent(body); return msg; } private MimeMessage makeEmail(TokenData data, _EmailTemplate template, List<_BridgeMessageContent> contents, boolean allowReply) throws MessagingException, IOException { MimeMultipart body = new MimeMultipart(); body.setSubType("alternative"); for (_BridgeMessageContent content : contents) { MimeMultipart contentBody = new MimeMultipart(); contentBody.setSubType("related"); Optional<_EmailTemplateContent> contentTemplateOpt = template.getContent(content.getMime()); if (!contentTemplateOpt.isPresent()) { continue; } _EmailTemplateContent contentTemplate = contentTemplateOpt.get(); contentBody.addBodyPart(makeBodyPart(data, contentTemplate, content)); if (contentTemplate.getContent().contains(EmailTemplateToken.SenderAvatar.getToken()) && data.getSenderAvatar() != null && data.getSenderAvatar().isValid()) { log.info("Adding avatar for sender"); MimeBodyPart avatarBp = new MimeBodyPart(); _MatrixContent avatar = data.getSenderAvatar(); String filename = avatar.getFilename() .map(f -> f.replace("image/", "").replace("\"", "")) .filter(StringUtils::isNotBlank) .orElseGet(() -> "unknown." + avatar.getType()); avatarBp.setContent(avatar.getData(), avatar.getType()); avatarBp.setContentID("<" + senderAvatarId + ">"); avatarBp.setDisposition("inline; filename=\"" + filename + "\"; size=" + avatar.getData().length + ";"); contentBody.addBodyPart(avatarBp); } MimeBodyPart part = new MimeBodyPart(); part.setContent(contentBody); body.addBodyPart(part); } return makeEmail(data, template, body, allowReply); } private MimeMessage makeEmail(TokenData data, _EmailTemplate template, boolean allowReply) throws IOException, MessagingException { List<_BridgeMessageContent> contents = Arrays.asList( new BridgeMessageContent(MimeTypeUtils.TEXT_PLAIN_VALUE), new BridgeMessageContent(MimeTypeUtils.TEXT_HTML_VALUE) ); return makeEmail(data, template, contents, allowReply); } @Override public Optional<MimeMessage> get(_BridgeSubscription sub, _MatrixBridgeMessage msg) throws IOException, MessagingException { Optional<_EmailTemplate> templateOpt = templateMgr.get(SubscriptionEvents.OnMessage); if (!templateOpt.isPresent()) { log.info("Ignoring message event {} to {}, no notification set", msg.getKey(), sub.getEmailEndpoint().getIdentity()); return Optional.empty(); } _EmailTemplate template = templateOpt.get(); List<_EmailTemplateContent> templates = template.listContents(); if (templates.isEmpty()) { log.info("No template configured for subscription event {}, skipping"); return Optional.empty(); } Optional<_BridgeMessageContent> txtOpt = msg.getContent(MimeTypeUtils.TEXT_PLAIN_VALUE); Optional<_BridgeMessageContent> htmlOpt = msg.getContent(MimeTypeUtils.TEXT_HTML_VALUE); List<_BridgeMessageContent> contents = new ArrayList<>(); if (!txtOpt.isPresent()) { if (!htmlOpt.isPresent()) { log.warn("Ignoring Matrix message {} to {}, no valid content", msg.getKey(), sub.getEmailEndpoint().getIdentity()); return Optional.empty(); } contents.add(htmlOpt.get()); } else { contents.add(txtOpt.get()); if (htmlOpt.isPresent()) { contents.add(htmlOpt.get()); } else { contents.add(new BridgeMessageHtmlContent(getHtml(txtOpt.get().getContentAsString()))); } } // TODO refactor with duplicated code within get() _MatrixClient mxClient = sub.getMatrixEndpoint().getClient(); _MatrixUser userSource = msg.getSender(); Optional<_MatrixContent> userAvatar = userSource.getAvatarThumbnail(48, 48); LocalDateTime ldt = LocalDateTime.ofInstant(msg.getTime(), ZoneOffset.systemDefault()); TokenData tokenData = new TokenData(sub.getEmailEndpoint().getChannelId()); tokenData.setManageUrl(getSubscriptionManageLink(sub.getEmailEndpoint().getChannelId())); tokenData.setTimeHour(ldt.format(hourFormatter)); tokenData.setTimeMin(ldt.format(minFormatter)); tokenData.setTimeSec(ldt.format(secFormatter)); tokenData.setSenderAddress(userSource.getId().getId()); tokenData.setSenderName(userSource.getName().orElse("")); userAvatar.ifPresent(tokenData::setSenderAvatar); tokenData.setSender(StringUtils.defaultIfBlank(tokenData.getSenderName(), tokenData.getSenderAddress())); tokenData.setReceiverAddress(sub.getEmailEndpoint().getIdentity()); tokenData.setRoomAddress(sub.getMatrixEndpoint().getChannelId()); tokenData.setRoomName(mxClient.getRoom(tokenData.getRoomAddress()).getName().orElse("")); tokenData.setRoom(StringUtils.defaultIfBlank(tokenData.getRoomName(), tokenData.getRoomAddress())); tokenData.setSelf(sub.getMatrixEndpoint().getClient().getUser().equals(msg.getSender())); return Optional.of(makeEmail(tokenData, template, contents, true)); } @Override public Optional<MimeMessage> get(_SubscriptionEvent ev) throws IOException, MessagingException { Optional<_EmailTemplate> templateOpt = templateMgr.get(ev.getType()); if (!templateOpt.isPresent()) { log.info("Ignoring subscription event {} to {}, no notification set", ev.getType(), ev.getSubscription().getEmailEndpoint().getIdentity()); return Optional.empty(); } _EmailTemplate template = templateOpt.get(); List<_EmailTemplateContent> templates = template.listContents(); if (templates.isEmpty()) { log.info("No template configured for subscription event {}, skipping"); return Optional.empty(); } _MatrixClient mxClient = ev.getSubscription().getMatrixEndpoint().getClient(); _MatrixUser userSource = mxClient.getUser(new MatrixID(ev.getInitiator())); Optional<_MatrixContent> userAvatar = userSource.getAvatarThumbnail(48, 48); LocalDateTime ldt = LocalDateTime.ofInstant(ev.getTime(), ZoneOffset.systemDefault()); TokenData tokenData = new TokenData(ev.getSubscription().getEmailEndpoint().getChannelId()); tokenData.setManageUrl(getSubscriptionManageLink(ev.getSubscription().getEmailEndpoint().getChannelId())); tokenData.setTimeHour(ldt.format(hourFormatter)); tokenData.setTimeMin(ldt.format(minFormatter)); tokenData.setTimeSec(ldt.format(secFormatter)); tokenData.setSenderAddress(userSource.getId().getId()); tokenData.setSenderName(userSource.getName().orElse("")); userAvatar.ifPresent(tokenData::setSenderAvatar); tokenData.setSender(StringUtils.defaultIfBlank(tokenData.getSenderName(), tokenData.getSenderAddress())); tokenData.setReceiverAddress(ev.getSubscription().getEmailEndpoint().getIdentity()); tokenData.setRoomAddress(ev.getSubscription().getMatrixEndpoint().getChannelId()); tokenData.setRoomName(mxClient.getRoom(tokenData.getRoomAddress()).getName().orElse("")); tokenData.setRoom(StringUtils.defaultIfBlank(tokenData.getRoomName(), tokenData.getRoomAddress())); tokenData.setSelf(StringUtils.equalsIgnoreCase(ev.getInitiator(), ev.getSubscription().getMatrixEndpoint().getClient().getUser().getId())); switch (ev.getType()) { case OnCreate: return Optional.of(makeEmail(tokenData, template, true)); case OnDestroy: return Optional.of(makeEmail(tokenData, template, false)); default: log.warn("Unknown subscription event type {}, using default behaviour", ev.getType().getId()); return Optional.of(makeEmail(tokenData, template, false)); } } private class TokenData { private String key; private String timeHour; private String timeMin; private String timeSec; private String sender; private String senderName; private String senderAddress; private _MatrixContent senderAvatar; private String receiverAddress; private String room; private String roomName; private String roomAddress; private String manageUrl; private boolean isSelf; TokenData(String key) { this.key = key; } String getKey() { return key; } String getTimeHour() { return timeHour; } void setTimeHour(String timeHour) { this.timeHour = timeHour; } String getTimeMin() { return timeMin; } void setTimeMin(String timeMin) { this.timeMin = timeMin; } String getTimeSec() { return timeSec; } void setTimeSec(String timeSec) { this.timeSec = timeSec; } public String getSender() { return sender; } public void setSender(String sender) { this.sender = sender; } String getSenderName() { return senderName; } void setSenderName(String senderName) { this.senderName = senderName; } String getSenderAddress() { return senderAddress; } void setSenderAddress(String senderAddress) { this.senderAddress = senderAddress; } _MatrixContent getSenderAvatar() { return senderAvatar; } void setSenderAvatar(_MatrixContent senderAvatar) { this.senderAvatar = senderAvatar; } String getReceiverAddress() { return receiverAddress; } void setReceiverAddress(String receiverAddress) { this.receiverAddress = receiverAddress; } String getRoom() { return room; } void setRoom(String room) { this.room = room; } String getRoomName() { return roomName; } void setRoomName(String roomName) { this.roomName = roomName; } String getRoomAddress() { return roomAddress; } void setRoomAddress(String roomAddress) { this.roomAddress = roomAddress; } String getManageUrl() { return manageUrl; } void setManageUrl(String manageUrl) { this.manageUrl = manageUrl; } public boolean isSelf() { return isSelf; } public void setSelf(boolean self) { isSelf = self; } } }