All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.oliviercailloux.grade.comm.Emailer Maven / Gradle / Ivy

The newest version!
package io.github.oliviercailloux.grade.comm;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static io.github.oliviercailloux.email.UncheckedMessagingException.MESSAGING_UNCHECKER;

import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.MoreCollectors;
import io.github.oliviercailloux.email.EmailAddressAndPersonal;
import io.github.oliviercailloux.email.ImapSearchPredicate;
import io.github.oliviercailloux.email.UncheckedMessagingException;
import io.github.oliviercailloux.xml.XmlUtils;
import jakarta.mail.FetchProfile;
import jakarta.mail.Folder;
import jakarta.mail.Message;
import jakarta.mail.Message.RecipientType;
import jakarta.mail.MessagingException;
import jakarta.mail.NoSuchProviderException;
import jakarta.mail.Part;
import jakarta.mail.Session;
import jakarta.mail.Store;
import jakarta.mail.Transport;
import jakarta.mail.URLName;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.stream.StreamSupport;
import org.eclipse.angus.mail.imap.IMAPFolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The time from connectToStore to close is supposed to be short. Meanwhile, this object might keep
 * open folders or other resources.
 *
 * @author Olivier Cailloux
 *
 */
public class Emailer implements AutoCloseable {
  @SuppressWarnings("unused")
  private static final Logger LOGGER = LoggerFactory.getLogger(Emailer.class);

  @SuppressWarnings("unused")
  private static final Logger LOGGER_JAVAMAIL =
      LoggerFactory.getLogger(Emailer.class.getCanonicalName() + ".Javamail");

  private static final PrintStream JAVAMAIL_LOGGING_OUTPUT_STREAM =
      LoggingOutputStream.newInstance(LOGGER_JAVAMAIL);

  public static Emailer instance() {
    return new Emailer();
  }

  public static Session getOutlookImapSession() {
    final Properties props = new Properties();
    props.setProperty("mail.store.protocol", "imap");
    props.setProperty("mail.host", "outlook.office365.com");
    props.setProperty("mail.imap.connectiontimeout", "2000");
    props.setProperty("mail.imap.timeout", "60*1000");
    props.setProperty("mail.imap.connectionpooltimeout", "10");
    props.setProperty("mail.imap.ssl.enable", "true");
    props.setProperty("mail.imap.ssl.checkserveridentity", "true");
    final Session session = Session.getInstance(props);
    return session;
  }

  public static Session getGmailImapSession() {
    final Properties props = new Properties();
    props.setProperty("mail.store.protocol", "imap");
    props.setProperty("mail.host", "imap.gmail.com");
    props.setProperty("mail.imap.connectiontimeout", "2000");
    props.setProperty("mail.imap.timeout", "60000");
    props.setProperty("mail.imap.connectionpooltimeout", "10");
    props.setProperty("mail.imap.ssl.enable", "true");
    props.setProperty("mail.imap.ssl.checkserveridentity", "true");
    final Session session = Session.getInstance(props);
    return session;
  }

  public static Session getZohoImapSession() {
    final Properties props = new Properties();
    props.setProperty("mail.store.protocol", "imap");
    props.setProperty("mail.host", "imap.zoho.eu");
    props.setProperty("mail.imap.connectiontimeout", "2000");
    props.setProperty("mail.imap.timeout", "60000");
    props.setProperty("mail.imap.connectionpooltimeout", "10");
    props.setProperty("mail.imap.ssl.enable", "true");
    props.setProperty("mail.imap.ssl.checkserveridentity", "true");
    final Session session = Session.getInstance(props);
    return session;
  }

  public static Session getOutlookSmtpSession() {
    final Properties props = new Properties();
    props.setProperty("mail.store.protocol", "smtp");
    props.setProperty("mail.host", "outlook.office365.com");
    props.setProperty("mail.smtp.auth", "true");
    props.setProperty("mail.smtp.starttls.enable", "true");
    props.setProperty("mail.smtp.port", "587");
    final Session session = Session.getInstance(props);
    return session;
  }

  public static ImmutableMap byMessageNumber(Iterable messages) {
    return StreamSupport.stream(messages.spliterator(), false).collect(ImmutableMap
        .toImmutableMap(MESSAGING_UNCHECKER.wrapFunction(Message::getMessageNumber), m -> m));
  }

  public static String getDescription(Message message) {
    return String.format("Message number %s sent %s to %s with subject '%s'.",
        message.getMessageNumber(), MESSAGING_UNCHECKER.getUsing(message::getSentDate),
        ImmutableList
            .copyOf(MESSAGING_UNCHECKER.getUsing(() -> message.getRecipients(RecipientType.TO))),
        MESSAGING_UNCHECKER.getUsing(message::getSubject));
  }

  private Store store;
  private final LinkedHashMap openReadFolders;
  private final LinkedHashMap openRwFolders;

  private Session transportSession;
  private Transport transport;
  private Folder saveInto;

  private Emailer() {
    store = null;
    openReadFolders = Maps.newLinkedHashMap();
    openRwFolders = Maps.newLinkedHashMap();
    transportSession = null;
    transport = null;
    saveInto = null;
  }

  public void connectToStore(Session session, String username, String password) {
    try {
      session.setDebugOut(JAVAMAIL_LOGGING_OUTPUT_STREAM);
      session.setDebug(true);
      store = session.getStore();
      LOGGER.info("Connecting to store with properties {}.", session.getProperties());
      MESSAGING_UNCHECKER.call(() -> store.connect(username, password));
      LOGGER.info("Connected to store.");
    } catch (NoSuchProviderException e) {
      throw new IllegalStateException(e);
    }
  }

  public URLName getUrlName() {
    checkState(store != null);
    return store.getURLName();
  }

  public void connectToTransport(Session session, String username, String password) {
    transportSession = checkNotNull(session);
    try {
      transport = transportSession.getTransport();
      LOGGER.info("Connecting to transport.");
      MESSAGING_UNCHECKER.call(() -> transport.connect(username, password));
      LOGGER.info("Connected to transport.");
    } catch (NoSuchProviderException e) {
      throw new IllegalStateException(e);
    }
  }

  @SuppressWarnings("resource")
  public ImmutableList getFolderNames() {
    checkState(store != null);

    try {
      /* We should NOT close this folder as it has (rightly) not been opened. */
      final Folder root = store.getDefaultFolder();
      /*
       * Let’s return strings instead of Folders to avoid tempting the user in opening the folder
       * themselves (we want to control this).
       */
      return Arrays.stream(root.list()).map(Folder::getName)
          .collect(ImmutableList.toImmutableList());
    } catch (MessagingException e) {
      throw new UncheckedMessagingException(e);
    }
  }

  /**
   * 

* Returns the given folder, if it exists, after having opened it in READ mode if this object had * not opened it already. (If this object had already opened the folder in READ_WRITE mode, then * it is returned open in that state.) *

*

* This object will take care of closing the folder. *

* * @return an opened folder. */ public Folder getFolder(String name) { return lazyGetFolder(name, false); } /** *

* Returns the given folder, if it exists, after having opened it in READ_WRITE mode if this * object had not opened it already in READ_WRITE mode. *

*

* This object will take care of closing the folder. *

* * @throws IllegalStateException if the given folder is already opened in READ mode. */ public Folder getFolderReadWrite(String name) throws IllegalStateException { return lazyGetFolder(name, true); } private Folder lazyGetFolder(String folderName, boolean andWrite) throws IllegalStateException { checkState(store != null); if (!openReadFolders.containsKey(folderName) && !openRwFolders.containsKey(folderName)) { @SuppressWarnings("resource") final Folder folder = MESSAGING_UNCHECKER.getUsing(() -> store.getFolder(folderName)); if (andWrite) { MESSAGING_UNCHECKER.call(() -> folder.open(Folder.READ_WRITE)); openRwFolders.put(folderName, folder); } else { MESSAGING_UNCHECKER.call(() -> folder.open(Folder.READ_ONLY)); openReadFolders.put(folderName, folder); } } final Folder folderRead = openReadFolders.get(folderName); if (folderRead != null) { /* * NB this means that opening a folder read-only for searching prevents from storing in that * folder: planning is required. */ checkState(!andWrite, "A given folder can be opened only once."); return folderRead; } return openRwFolders.get(folderName); } public ImmutableSet searchIn(Folder folder, ImapSearchPredicate term) { if (term.equals(ImapSearchPredicate.FALSE)) { return ImmutableSet.of(); } checkState(folder.isOpen()); final Message[] asArray = MESSAGING_UNCHECKER.getUsing(() -> folder.search(term.getTerm())); final ImmutableSet found = ImmutableSet.copyOf(asArray); LOGGER.info("Searched for {}, got: {}.", term, found.size()); fetchHeaders(folder, found); /* TODO Searching for "Grade Java" finds "Grade Projet Java". */ final boolean filter = false; if (filter) { return found.stream().filter(term.getPredicate()).collect(ImmutableSet.toImmutableSet()); } final Optional notMatching = found.stream().filter(term.getPredicate().negate()) .limit(1).collect(MoreCollectors.toOptional()); if (notMatching.isPresent()) { throw new VerifyException(getDescription(notMatching.get())); } LOGGER.debug("Verified {}.", found.size()); return found; } public void fetchHeaders(Folder folder, Set messages) { fetch(folder, messages, false); } public void fetchMessages(Folder folder, Set messages) { fetch(folder, messages, true); } private void fetch(Folder folder, Set messages, boolean whole) { final FetchProfile fp = new FetchProfile(); fp.add(FetchProfile.Item.CONTENT_INFO); fp.add(FetchProfile.Item.ENVELOPE); fp.add(FetchProfile.Item.FLAGS); fp.add(FetchProfile.Item.SIZE); fp.add(IMAPFolder.FetchProfileItem.HEADERS); fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE); if (whole) { fp.add(IMAPFolder.FetchProfileItem.MESSAGE); } try { folder.fetch(messages.toArray(new Message[messages.size()]), fp); LOGGER.debug("Fetched {} " + (whole ? "messages" : "headers") + ".", messages.size()); } catch (MessagingException e) { throw new IllegalStateException(e); } } private MimeMessage getMessage(Email email, InternetAddress fromAddress) { checkNotNull(transportSession); final String subject = email.getSubject(); final String textContent = XmlUtils.asString(email.getDocument()); final String utf8 = StandardCharsets.UTF_8.name(); final MimeMessage message = new MimeMessage(transportSession); try { message.setFrom(fromAddress); message.setSubject(subject, utf8); final MimeBodyPart textPart = new MimeBodyPart(); textPart.setText(textContent, utf8, "html"); final MimeMultipart multipartContent; if (email.hasFile()) { final String fileName = email.getFileName(); final String fileContent = email.getFileContent(); final String fileSubtype = email.getFileSubtype(); final MimeBodyPart filePart = new MimeBodyPart(); filePart.setFileName(fileName); filePart.setDisposition(Part.ATTACHMENT); filePart.setText(fileContent, utf8, fileSubtype); multipartContent = new MimeMultipart(textPart, filePart); } else { multipartContent = new MimeMultipart(textPart); } message.setContent(multipartContent); final EmailAddressAndPersonal to = email.getTo(); final InternetAddress[] toSingleton = new InternetAddress[] {to.asInternetAddress()}; /* * When the address is incorrect (e.g. [email protected]), a message delivered event is still * sent to registered TransportListeners. A new message in the INBOX indicates the error, but * it seems hard to detect it on the spot. */ message.setRecipients(Message.RecipientType.TO, toSingleton); message.saveChanges(); } catch (MessagingException e) { throw new UncheckedMessagingException(e); } return message; } public void saveInto(Folder folder) { this.saveInto = folder; } public void send(Collection emails, EmailAddressAndPersonal fromAddress) { if (saveInto != null && !emails.isEmpty()) { checkState(transport != null); checkState(saveInto.isOpen()); checkState(saveInto.getMode() == Folder.READ_WRITE); } for (Email email : emails) { final MimeMessage message = getMessage(email, fromAddress.asInternetAddress()); MESSAGING_UNCHECKER.call(() -> transport.sendMessage(message, message.getAllRecipients())); MESSAGING_UNCHECKER.call(() -> saveInto.appendMessages(new Message[] {message})); } LOGGER.info("Messages sent: {}.", emails.size()); } @Override public void close() throws UncheckedMessagingException { if (transportSession != null) { MESSAGING_UNCHECKER.call(() -> transport.close()); } for (Folder folder : openRwFolders.values()) { MESSAGING_UNCHECKER.call(() -> folder.close()); } openRwFolders.clear(); for (Folder folder : openReadFolders.values()) { MESSAGING_UNCHECKER.call(() -> folder.close()); } openReadFolders.clear(); if (store != null) { MESSAGING_UNCHECKER.call(() -> store.close()); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy