| /* |
| ******************************************************************************* |
| * 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; |
| } |
| |
| } |