blob: fad89fa917996d6c3cce6153367991e52d48c8a1 [file] [log] [blame]
/*
*******************************************************************************
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************
*/
package org.eclipse.openk.statementpublicaffairs.service.mail;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Store;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;
import javax.mail.search.MessageIDTerm;
import javax.mail.search.SearchTerm;
import org.apache.commons.io.IOUtils;
import org.eclipse.openk.statementpublicaffairs.api.MailUtil;
import org.eclipse.openk.statementpublicaffairs.exceptions.ConfigurationException;
import org.eclipse.openk.statementpublicaffairs.exceptions.InternalErrorServiceException;
import org.eclipse.openk.statementpublicaffairs.exceptions.NotFoundServiceException;
import org.eclipse.openk.statementpublicaffairs.model.AttachmentFile;
import org.eclipse.openk.statementpublicaffairs.model.mail.MailEntry;
import org.eclipse.openk.statementpublicaffairs.model.mail.NewMailContext;
import org.eclipse.openk.statementpublicaffairs.util.TypeConversion;
import org.eclipse.openk.statementpublicaffairs.viewmodel.AttachmentModel;
import org.eclipse.openk.statementpublicaffairs.viewmodel.MailSendReport;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.util.StreamUtils;
import lombok.extern.java.Log;
@Log
public class MailContext {
private static final String IMAP = "imap";
private static final String MESSAGE_ID = "Message-ID";
private static final String MULTIPART = "multipart/*";
private static final String CLOSING_INBOX_FAILED = "Closing inbox failed. - ";
private static final String IMAP_FOLDER_INBOX = "INBOX";
private static final String IMAP_FOLDER_PROCESSED_STATEMENTS = "Processed Statements";
private static final String IMAP_FOLDER_TRASH = "Trash";
private final Object syncobject = new Object();
private MailSession session;
private MailUtil mailUtil;
public MailContext(MailUtil mailUtil, String propertiesPath) throws ConfigurationException {
this.mailUtil = mailUtil;
this.session = mailUtil.sessionFor(propertiesPath);
System.setProperty("mail.mime.splitlongparameters", "false");
}
public MailSession getSession() {
return session;
}
public String getSender() {
return this.session.getProperty("mail.smtp.from");
}
public MimeMessage newMessage() {
return getSession().newMessage();
}
public Store getStore() throws MessagingException {
Store store = session.getStore(IMAP);
if (!store.isConnected()) {
store.connect();
}
return store;
}
public Folder getInbox(Store store) throws MessagingException {
return store.getFolder(IMAP_FOLDER_INBOX);
}
public Folder getTrash(Store store) throws MessagingException {
return store.getFolder(IMAP_FOLDER_TRASH);
}
public Folder getProcessedStatementBox(Store store) throws MessagingException {
return store.getFolder(IMAP_FOLDER_PROCESSED_STATEMENTS);
}
public List<String> getInboxMessageIds(String filterMailToAddress) throws MessagingException {
try (Store store = getStore()) {
Folder inbox = getInbox(store);
inbox.open(Folder.READ_ONLY);
List<String> messageIds = new ArrayList<>();
for (Message m : inbox.getMessages()) {
if (filterMailToAddress != null) {
List<InternetAddress> to = Stream.of(m.getAllRecipients()).filter(InternetAddress.class::isInstance)
.map(InternetAddress.class::cast).collect(Collectors.toList());
boolean found = false;
for (InternetAddress addr : to) {
if (filterMailToAddress.equals(addr.getAddress())) {
found = true;
break;
}
}
if (!found) {
continue;
}
}
String[] s = m.getHeader(MESSAGE_ID);
if (s.length > 0) {
messageIds.add(s[0]);
}
}
inbox.close();
return messageIds;
}
}
public List<MailEntry> getInboxMessages(String filterMailToAddress) throws MessagingException {
try (Store store = getStore()) {
Folder inbox = getInbox(store);
inbox.open(Folder.READ_ONLY);
List<MailEntry> mails = new ArrayList<>();
for (Message m : inbox.getMessages()) {
if (toAddressMatches(m, filterMailToAddress)) {
parseMail(m).ifPresent(mails::add);
}
}
inbox.close();
return mails;
}
}
private String parseFrom(Message m) throws MessagingException {
Address[] from = m.getFrom();
if (from.length > 0) {
return from[0].toString();
}
return null;
}
private String parseMessageId(Message m) throws MessagingException {
String[] s = m.getHeader(MESSAGE_ID);
if (s.length > 0) {
return s[0];
}
return null;
}
private boolean toAddressMatches(Message m, String filterMailToAddress) throws MessagingException {
if (filterMailToAddress == null) {
return true;
}
List<InternetAddress> to = Stream.of(m.getAllRecipients()).filter(InternetAddress.class::isInstance)
.map(InternetAddress.class::cast).collect(Collectors.toList());
boolean found = false;
for (InternetAddress addr : to) {
if (filterMailToAddress.equals(addr.getAddress())) {
found = true;
break;
}
}
return found;
}
public Map<String, AttachmentFile> getAttachments(String messageId, Set<String> fileNames) {
Map<String, AttachmentFile> attachments = null;
String[] searchFolders = { IMAP_FOLDER_INBOX, IMAP_FOLDER_PROCESSED_STATEMENTS };
for (String folder : searchFolders) {
Optional<Map<String, AttachmentFile>> oAttachments = findAttachment(messageId, fileNames, folder);
if (oAttachments.isPresent()) {
attachments = oAttachments.get();
break;
}
}
if (attachments == null) {
attachments = new HashMap<>();
}
return attachments;
}
private Optional<Map<String, AttachmentFile>> findAttachment(String messageId, Set<String> fileNames,
String folderPath) {
Optional<Map<String, AttachmentFile>> result = Optional.empty();
try (Store store = getStore()) {
Folder folder = null;
try {
folder = store.getFolder(folderPath);
folder.open(Folder.READ_ONLY);
SearchTerm messageIdSearch = new MessageIDTerm(messageId);
Message[] messages = folder.search(messageIdSearch);
if (messages.length > 0) {
Message m = messages[0];
result = Optional.of(getAttachmentFilesFromMessage(m, fileNames));
}
folder.close();
} catch (MessagingException e) {
log.warning(CLOSING_INBOX_FAILED + e.getMessage());
}
if (folder.isOpen()) {
try {
folder.close();
} catch (MessagingException e) {
log.warning(CLOSING_INBOX_FAILED + e.getMessage());
}
}
} catch (MessagingException e) {
log.warning(CLOSING_INBOX_FAILED + e.getMessage());
}
return result;
}
protected Map<String, AttachmentFile> getAttachmentFilesFromMessage(Message m, Set<String> fileNames)
throws MessagingException {
Map<String, AttachmentFile> attachments;
if (m.isMimeType(MULTIPART)) {
try {
MimeMultipart mimeMultipart = (MimeMultipart) m.getContent();
attachments = getAttachmentFilesFromMimeMultipart(mimeMultipart, fileNames);
} catch (IOException e) {
log.warning("IOException when reading multipart content - " + e.getMessage());
attachments = new HashMap<>();
}
} else {
attachments = new HashMap<>();
}
return attachments;
}
private Map<String, AttachmentFile> getAttachmentFilesFromMimeMultipart(MimeMultipart mimeMultipart,
Set<String> fileNames) throws MessagingException {
Map<String, AttachmentFile> attachments = new HashMap<>();
int count = mimeMultipart.getCount();
for (int i = 0; i < count; i++) {
BodyPart bodyPart = mimeMultipart.getBodyPart(i);
try {
Object content = bodyPart.getContent();
String fileName = bodyPart.getFileName();
fileName = fileName != null ? getDecodeText(fileName) : null;
if (content instanceof MimeMultipart) {
attachments.putAll(getAttachmentFilesFromMimeMultipart((MimeMultipart) content, fileNames));
} else if (bodyPart.getFileName() != null && fileNames.contains(fileName)) {
AttachmentFile attachment = new AttachmentFile();
attachment.setType(bodyPart.getContentType());
attachment.setName(fileName);
InputStream in = bodyPart.getInputStream();
byte[] data = StreamUtils.copyToByteArray(in);
attachment.setLength(data.length);
InputStream ressource = new ByteArrayInputStream(data);
attachment.setRessource(ressource);
attachments.put(fileName, attachment);
}
} catch (IOException e) {
log.warning("IOException when reading attachment content - " + e.getMessage());
}
}
return attachments;
}
protected List<AttachmentModel> getAttachmentsFromMessage(Message m) throws IOException, MessagingException {
List<AttachmentModel> attachments;
if (m.isMimeType(MULTIPART)) {
MimeMultipart mimeMultipart = (MimeMultipart) m.getContent();
attachments = getAttachmentsFromMimeMultipart(mimeMultipart);
} else {
attachments = new ArrayList<>();
}
return attachments;
}
private List<AttachmentModel> getAttachmentsFromMimeMultipart(MimeMultipart mimeMultipart)
throws MessagingException {
List<AttachmentModel> attachments = new ArrayList<>();
int count = mimeMultipart.getCount();
for (int i = 0; i < count; i++) {
BodyPart bodyPart = mimeMultipart.getBodyPart(i);
try {
Object content = bodyPart.getContent();
if (content instanceof MimeMultipart) {
attachments.addAll(getAttachmentsFromMimeMultipart((MimeMultipart) content));
} else if (bodyPart.getFileName() != null) {
AttachmentModel attachment = new AttachmentModel();
attachment.setName(getDecodeText(bodyPart.getFileName()));
attachment.setType(bodyPart.getContentType());
attachment.setSize((long) bodyPart.getSize());
attachments.add(attachment);
}
} catch (IOException e) {
log.warning("IOException when reading attachment meta data - " + e.getMessage());
}
}
return attachments;
}
protected String getDecodeText(final String value) {
if (value == null) {
return "unknown";
}
try {
return MimeUtility.decodeText(value);
} catch (final UnsupportedEncodingException e) {
log.warning("Invalid encoding." + e.getMessage());
return "unknown";
}
}
protected String getTextFromMessage(Message message) throws MessagingException, IOException {
String result = "";
if (message.isMimeType("text/plain")) {
result = message.getContent().toString();
} else if (message.isMimeType(MULTIPART)) {
MimeMultipart mimeMultipart = (MimeMultipart) message.getContent();
result = getTextFromMimeMultipart(mimeMultipart);
}
return result;
}
private String getTextFromMimeMultipart(MimeMultipart mimeMultipart) throws MessagingException, IOException {
StringBuilder result = new StringBuilder();
int count = mimeMultipart.getCount();
for (int i = 0; i < count; i++) {
BodyPart bodyPart = mimeMultipart.getBodyPart(i);
if (bodyPart.isMimeType("text/plain")) {
result.append("\n").append(bodyPart.getContent());
} else if (bodyPart.isMimeType("text/html")) {
result.append("\n").append(bodyPart.getContent());
} else if (bodyPart.getContent() instanceof MimeMultipart) {
result.append(getTextFromMimeMultipart((MimeMultipart) bodyPart.getContent()));
}
}
return result.toString();
}
public void moveMail(String messageId, String sourceFolderPath, String destFolderPath)
throws InternalErrorServiceException, NotFoundServiceException, InterruptedException {
boolean found = false;
boolean deleted = false;
synchronized (syncobject) {
Folder sourceFolder = null;
Folder dstFolder = null;
try (Store store = getStore()) {
sourceFolder = store.getFolder(sourceFolderPath);
sourceFolder.open(Folder.READ_WRITE);
dstFolder = store.getFolder(destFolderPath);
dstFolder.open(Folder.READ_WRITE);
SearchTerm messageIdSearch = new MessageIDTerm(messageId);
Message[] messages = sourceFolder.search(messageIdSearch);
if (messages.length > 0) {
found = true;
Message m = messages[0];
Message[] delMessages = { m };
sourceFolder.copyMessages(messages, dstFolder);
synchronized (sourceFolder) {
sourceFolder.wait(1000);
}
sourceFolder.setFlags(delMessages, new Flags(Flags.Flag.DELETED), true);
sourceFolder.expunge();
deleted = true;
}
} catch (MessagingException e) {
log.warning("Error occurred accessing the statement mail inbox - " + e.getMessage());
}
if (sourceFolder.isOpen()) {
try {
sourceFolder.close();
} catch (MessagingException e) {
log.warning("Error occurred closing inbox folder - " + e.getMessage());
}
}
if (dstFolder != null && dstFolder.isOpen()) {
try {
dstFolder.close();
} catch (MessagingException e) {
log.warning("Error occurred closing statementFolder folder - " + e.getMessage());
}
}
}
if (!found) {
throw new NotFoundServiceException("Could not find message with messageId " + messageId);
}
if (!deleted) {
throw new InternalErrorServiceException("Error occurred deleting the message with messageId " + messageId);
}
}
public Optional<MailEntry> getMail(String messageId) {
if (messageId == null) {
return Optional.empty();
}
String[] searchFolders = { IMAP_FOLDER_INBOX, IMAP_FOLDER_PROCESSED_STATEMENTS };
for (String folder : searchFolders) {
Optional<MailEntry> oMail = findMail(messageId, folder);
if (oMail.isPresent()) {
return oMail;
}
}
return Optional.empty();
}
private Optional<MailEntry> findMail(String messageId, String folderPath) {
Optional<MailEntry> oMail = Optional.empty();
Folder folder = null;
try (Store store = getStore()) {
folder = store.getFolder(folderPath);
folder.open(Folder.READ_ONLY);
SearchTerm messageIdSearch = new MessageIDTerm(messageId);
Message[] messages = folder.search(messageIdSearch);
for (Message m : messages) {
String mId = parseMessageId(m);
if (messageId.equals(mId)) {
oMail = parseMail(m);
break;
}
}
} catch (MessagingException e) {
log.warning("Messaging exception when searching Mail - " + e.getMessage());
}
if (folder.isOpen()) {
try {
folder.close();
} catch (MessagingException e) {
log.warning("Messaging Exception when closing folder - " + e.getMessage());
}
}
return oMail;
}
private Optional<MailEntry> parseMail(Message m) {
Optional<MailEntry> oMail = Optional.empty();
MailEntry mail = new MailEntry();
try {
mail.setIdentifier(parseMessageId(m));
mail.setFrom(parseFrom(m));
TypeConversion.iso8601InstantStringOfDate(m.getSentDate()).ifPresent(mail::setDate);
mail.setSubject(m.getSubject());
mail.setTextPlain(getTextFromMessage(m));
mail.setAttachments(getAttachmentsFromMessage(m));
oMail = Optional.of(mail);
} catch (IOException e) {
log.warning("IOException when parsing mail text or attachments - " + e.getMessage());
} catch (MessagingException e) {
log.warning("MessagingException when parsing mail text or attachments - " + e.getMessage());
}
return oMail;
}
public void deleteMail(String messageId)
throws InternalErrorServiceException, NotFoundServiceException, InterruptedException {
moveMail(messageId, IMAP_FOLDER_INBOX, IMAP_FOLDER_TRASH);
}
public void setProcessedMail(String messageId)
throws InternalErrorServiceException, NotFoundServiceException, InterruptedException {
moveMail(messageId, IMAP_FOLDER_INBOX, IMAP_FOLDER_PROCESSED_STATEMENTS);
}
public MailSendReport sendMail(NewMailContext mail) {
MailSendReport report;
StringBuilder reason = new StringBuilder();
try {
MimeMessage msg = newMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg, true, "UTF-8");
List<InternetAddress> addressesTo = new ArrayList<>();
for (String recipient : mail.getRecipients()) {
InternetAddress addr = parseInternetAddress(recipient);
if (addr != null) {
addressesTo.add(addr);
}
}
if (addressesTo.isEmpty()) {
reason.append("No valid recipient email-address found.\n");
}
List<InternetAddress> bccAddressesTo = new ArrayList<>();
if (mail.getBccRecipients() != null) {
for (String bccRecipient : mail.getBccRecipients()) {
InternetAddress addr = parseInternetAddress(bccRecipient);
if (addr != null) {
bccAddressesTo.add(addr);
}
}
}
InternetAddress[] addressToArray = addressesTo.toArray(new InternetAddress[addressesTo.size()]);
helper.setTo(addressToArray);
InternetAddress[] bccAddressToArray = bccAddressesTo.toArray(new InternetAddress[bccAddressesTo.size()]);
helper.setBcc(bccAddressToArray);
helper.setSubject(mail.getSubject());
helper.setText(mail.getMailText());
if (mail.getAttachments() != null) {
for (AttachmentFile attachment : mail.getAttachments()) {
helper.addAttachment(MimeUtility.encodeWord(attachment.getName()),
new ByteArrayResource(IOUtils.toByteArray(attachment.getRessource())),
attachment.getType());
}
}
report = mailUtil.sendMail(msg);
} catch (MessagingException | IOException e) {
String errorMessage = "Error when sending notfication email: " + e.getMessage();
log.warning(errorMessage);
report = new MailSendReport();
report.setSuccessful(false);
report.setReason(errorMessage);
}
return report;
}
private InternetAddress parseInternetAddress(String recipient) {
try {
return new InternetAddress(recipient);
} catch (AddressException e) {
log.warning("Invalid recipient mail address " + recipient);
}
return null;
}
}