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

com.github.edgarespina.mwa.mail.MailBuilder Maven / Gradle / Ivy

The newest version!
package com.github.edgarespina.mwa.mail;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.text.MessageFormat;
import java.util.EnumSet;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.web.util.UriComponentsBuilder;

import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;

/**
 * Helper class for sending rich mail messages.
 *
 * @author edgar.espina
 * @since 0.1.3
 */
public class MailBuilder {

  /**
   * Extended the {@link BufferedInputStream} by adding a content-type
   * attribute.
   *
   * @author edgar.espina
   * @since 0.1.3
   */
  private static class InputStreamWithContentType extends BufferedInputStream {
    /**
     * The content-type mark.
     */
    private String contentType;

    /**
     * Creates a new {@link InputStreamWithContentType}.
     *
     * @param in The {@link InputStream}.
     * @param contentType The content-type.
     */
    public InputStreamWithContentType(final InputStream in,
        final String contentType) {
      super(in);
      this.contentType = contentType;
    }

  }

  /**
   * What kind of embedded/inline types are required.
   *
   * @author [email protected]
   * @since 0.1.3
   */
  private static enum EmbeddedType {
    /**
     * Plain text.
     */
    DEFAULT(Object.class) {
      /**
       * {@inheritDoc}
       */
      @Override
      public void append(final MimeMessageHelper message, final String cid,
          final Object embedded, final String contentType)
          throws MessagingException {
        throw new UnsupportedOperationException();
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public Object apply(final Object value) {
        String candidate = String.valueOf(value);
        if (candidate.startsWith("http://") || candidate.startsWith("https://")) {
          return UriComponentsBuilder.fromHttpUrl(candidate).build().encode()
              .toString();
        }
        return value;
      }
    },

    /**
     * Resource types.
     */
    RESOURCE(Resource.class) {
      /**
       * {@inheritDoc}
       */
      @Override
      public void append(final MimeMessageHelper message, final String id,
          final Object embedded, final String contentType)
          throws MessagingException {
        message.addInline(id, (Resource) embedded);
      }

    },

    /**
     * File types.
     */
    FILE(File.class) {
      /**
       * {@inheritDoc}
       */
      @Override
      public void append(final MimeMessageHelper message, final String id,
          final Object embedded, final String contentType)
          throws MessagingException {
        message.addInline(id, (File) embedded);
      }

    },

    /**
     * Input stream.
     */
    INPUT_STREAM(InputStream.class) {
      /**
       * {@inheritDoc}
       */
      @Override
      public void append(final MimeMessageHelper message, final String id,
          final Object embedded, final String contentType)
          throws MessagingException, IOException {
        message.addInline(id, toByteArrayResource((InputStream) embedded),
            contentType);
      }
    };

    /**
     * The embedded type.
     */
    private Class embeddedType;

    /**
     * Creates a new {@link EmbeddedType}.
     *
     * @param embeddedType The embedded type.
     */
    private EmbeddedType(final Class embeddedType) {
      this.embeddedType = embeddedType;
    }

    /**
     * Append the given resource to the provided message.
     *
     * @param message The message.
     * @param cid The resource id.
     * @param embedded The inline/embedded resource.
     * @param contentType The resource content-type.
     * @throws MessagingException If the embedded/inline resource cannot be
     *           added.
     */
    public abstract void append(MimeMessageHelper message, String cid,
        Object embedded, String contentType) throws MessagingException,
        IOException;

    /**
     * Pre-format the given value.
     *
     * @param value The value to be formatted.
     * @return A formatted value.
     */
    public Object apply(final Object value) {
      return cid + value;
    }

    /**
     * Return the type of the given embedded.
     *
     * @param embedded The embedded/inline resource.
     * @return The type of the given embedded.
     */
    public static EmbeddedType of(final Object embedded) {
      Validate.notNull(embedded, "Embedded object is required.");
      EnumSet supported = EnumSet.allOf(EmbeddedType.class);
      supported.remove(EmbeddedType.DEFAULT);
      for (EmbeddedType embeddedValue : supported) {
        if (embeddedValue.embeddedType.isInstance(embedded)) {
          return embeddedValue;
        }
      }
      return EmbeddedType.DEFAULT;
    }
  }

  /**
   * The Spring mail message helper.
   */
  private MimeMessageHelper message;

  /**
   * The logging system.
   */
  private static final Logger logger = LoggerFactory
      .getLogger(MailBuilder.class);

  /**
   * The cid prefix for inline resources.
   */
  private static final String cid = "cid:";

  /**
   * Creates a new {@link MailBuilder}.
   *
   * @param message The mail mime-message.
   * @throws MessagingException If something goes wrong.
   */
  private MailBuilder(final MimeMessageHelper message)
      throws MessagingException {
    this.message = message;
  }

  /**
   * Set the given text directly as content.
   * Always applies the default content type "text/plain".
   * Optionally, you can send a message template and pass some argument for
   * merging and create the final message.
   * 

* Usage: *

* *
   * text("Hello {0}!", "World");
   * 
* * @param text The text for the message. * @return This {@link MailBuilder}. * @throws MessagingException in case of errors. * @see MessageFormat#format */ public MailBuilder text(final String text, final Object... args) throws MessagingException { String formattedText = MessageFormat.format(text, processArgs(args)); message.setText(formattedText); logger.debug("mail-body: {}", formattedText); return this; } /** * Set 'to' with the provided email addresses. * * @param to The email addresses. * @return This {@link MailBuilder}. * @throws MessagingException */ public MailBuilder to(final String... to) throws MessagingException { logger.debug("mail-to: {}", Joiner.on(", ").join(to)); message.setTo(to); return this; } /** * Set 'to' with the provided email addresses. * * @param to The email addresses. * @return This {@link MailBuilder}. * @throws MessagingException */ public MailBuilder to(final Iterable to) throws MessagingException { return to(Iterables.toArray(to, String.class)); } /** * Set 'bcc' with the provided email addresses. * * @param bcc The email addresses. * @return This {@link MailBuilder}. * @throws MessagingException */ public MailBuilder bcc(final String... bcc) throws MessagingException { logger.debug("mail-bcc: {}", Joiner.on(",").join(bcc)); message.setBcc(bcc); return this; } /** * Set 'bcc' with the provided email addresses. * * @param bcc The email addresses. * @return This {@link MailBuilder}. * @throws MessagingException */ public MailBuilder bcc(final Iterable bcc) throws MessagingException { return bcc(Iterables.toArray(bcc, String.class)); } /** * Set cc with the provided email addresses. * * @param cc The email addresses. * @return This {@link MailBuilder}. * @throws MessagingException */ public MailBuilder cc(final String... cc) throws MessagingException { logger.debug("mail-cc: {}", Joiner.on(",").join(cc)); message.setCc(cc); return this; } /** * Set cc with the provided email addresses. * * @param cc The email addresses. * @return This {@link MailBuilder}. * @throws MessagingException */ public MailBuilder cc(final Iterable cc) throws MessagingException { return cc(Iterables.toArray(cc, String.class)); } /** * Set 'from' with the provided email address. * * @param from The email address. * @return This {@link MailBuilder}. * @throws MessagingException */ public MailBuilder from(final String from) throws MessagingException { logger.debug("mail-from: {}", from); message.setFrom(from); return this; } /** * Set 'from' with the provided email address. * * @param from The email address. * @param name The personal name. * @return This {@link MailBuilder}. * @throws MessagingException */ public MailBuilder from(final String from, final String name) throws MessagingException, UnsupportedEncodingException { message.setFrom(from, name); logger.debug("mail-from: \"{}\" <{}>", name, from); return this; } /** * Set 'replayTo' with the provided email address. * * @param replayTo The email address. * @return This {@link MailBuilder}. * @throws MessagingException */ public MailBuilder replyTo(final String replayTo) throws MessagingException { logger.debug("mail-replayTo: {}", replayTo); message.setReplyTo(replayTo); return this; } /** * Set 'replayTo' with the provided email address. * * @param replayTo The email address. * @param name The personal name. * @return This {@link MailBuilder}. * @throws MessagingException */ public MailBuilder replyTo(final String replayTo, final String name) throws MessagingException, UnsupportedEncodingException { logger.debug("mail-replayTo: \"{}\" <{}>", name, replayTo); message.setReplyTo(replayTo, name); return this; } /** * Set 'subject' of the message. * * @param subject The email address. * @return This {@link MailBuilder}. * @throws MessagingException */ public MailBuilder subject(final String subject) throws MessagingException { logger.debug("mail-subject: {}", subject); message.setSubject(subject); return this; } /** * Set the given text directly as content in non-multipart mode * or as default body part in multipart mode. * The "html" flag determines the content type to apply. *

* Usage: *

* *
   *  html("<img src=\"{0}\">"<p>Hello {1}!</p>,
   *    new File("header.jpg"),
   *    "World");
   * 
* * Or: * *
   *  html("<img src=\"{0}\">"<p>Hello {1}!</p>,
   *    MailBuilder.mailInputStream(source, "image/jpg"),
   *    "World");
   * 
* * @param html The html code as string. * @param args The embedded elements. Optional. * @return This {@link MailBuilder}. * @throws MessagingException If something goes wrong. * @throws IOException If the embedded/inline resource cannot be read it. */ public MailBuilder html(final String html, final Object... args) throws MessagingException, IOException { // The order is important so we need to set the text first and append the // inline later. // Step 1: Calculate resource id (if any) Object[] msgArgs = processArgs(args); // Step 2: Set the text with the resource ids. String formattedText = MessageFormat.format(html, msgArgs); logger.debug("mail-body: {}", formattedText); message.setText(formattedText, true); // Step 3: add embedded/inline for (int i = 0; i < args.length; i++) { EmbeddedType embeddedType = EmbeddedType.of(args[i]); if (embeddedType != EmbeddedType.DEFAULT) { String contentType = null; if (args[i] instanceof InputStream) { contentType = contentTypeOf((InputStream) args[i]); } embeddedType.append(message, ((String) msgArgs[i]).substring(cid.length()), args[i], contentType); } } return this; } /** * Add an attachment to the MimeMessage, taking the content from a * java.io.File. *

* The content type will be determined by the name of the given content file. * Do not use this for temporary files with arbitrary filenames (possibly * ending in ".tmp" or the like)! * * @param name The attachment name. * @param file The resource to add. * @return This {@link MailBuilder}. * @throws MessagingException If something goes wrong. */ public MailBuilder attach(final String name, final File file) throws MessagingException { logger.debug("mail-attachment: {}={}", name, file); message.addAttachment(name, file); return this; } /** * Add an attachment to the MimeMessage, taking the content from a * java.io.File. *

* The content type will be determined by the name of the given content file. * Do not use this for temporary files with arbitrary filenames (possibly * ending in ".tmp" or the like)! * * @param file The resource to add. * @return This {@link MailBuilder}. * @throws MessagingException If something goes wrong. */ public MailBuilder attach(final File file) throws MessagingException { logger.debug("mail-attachment: {}", file); message.addAttachment(file.getName(), file); return this; } /** * Add an attachment to the MimeMessage. * * @param name The attachment name. * @param input The resource to add. * @param contentType The resource content type. * @return This {@link MailBuilder}. * @throws MessagingException If something goes wrong. * @throws IOException If something goes wrong. */ public MailBuilder attach(final String name, final InputStream input, final String contentType) throws MessagingException, IOException { logger.debug("mail-attachment: {}={}", name, contentType); message.addAttachment(name, toByteArrayResource(input), contentType); return this; } /** * Add an attachment to the MimeMessage. * * @param name The attachment name. * @param resource The resource to add. * @param contentType The resource content type. * @return This {@link MailBuilder}. * @throws MessagingException If something goes wrong. */ public MailBuilder attach(final String name, final Resource resource, final String contentType) throws MessagingException { logger.debug("mail-attachment: {}={}", name, contentType); message.addAttachment(name, resource, contentType); return this; } /** * Return a {@link MimeMessage} ready to be send. * * @return A {@link MimeMessage} fully configured. */ public MimeMessage build() { return message.getMimeMessage(); } /** * Creates a new mail message with multi-part support. * * @param sender The email sender. Required. * @return A new mail message with multi-part support. * @throws MessagingException If the message cannot be created. */ public static MailBuilder newMail(final JavaMailSender sender) throws MessagingException { Validate.notNull(sender, "The email sender is required."); return new MailBuilder(new MimeMessageHelper(sender.createMimeMessage(), true)); } /** * Creates a new simple mail message without multi-part support. * * @param sender The email sender. Required. * @return A new mail message without multi-part support. * @throws MessagingException If the message cannot be created. */ public static MailBuilder newSimpleMail(final JavaMailSender sender) throws MessagingException { Validate.notNull(sender, "The email sender is required."); return new MailBuilder(new MimeMessageHelper(sender.createMimeMessage())); } /** * Detect the content-type of the {@link InputStream}. * * @param input The {@link InputStream} candidate. * @return The content-type. Default is: 'application/octet-stream'. */ private static String contentTypeOf(final InputStream input) { if (input instanceof InputStreamWithContentType) { return ((InputStreamWithContentType) input).contentType; } return "application/octet-stream"; } /** * Add content-type information to an {@link InputStream}. * * @param input The candidate input stream. Required. * @param contentType The content-type. Required. * @return An input-stream with content-type information. */ public static InputStream mailInputStream(final InputStream input, final String contentType) { Validate.notNull(input, "The input stream is required."); Validate.notEmpty(contentType, "The content-type is required."); return new InputStreamWithContentType(input, contentType); } /** * Transform the input to a {@link ByteArrayResource}. * * @param input The source stream. * @throws IOException If something goes wrong. */ private static ByteArrayResource toByteArrayResource(final InputStream input) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); int b; while (-1 != (b = input.read())) { output.write(b); } return new ByteArrayResource(output.toByteArray()); } /** * Process the list of arguments. There are special considerations for arg of * type: {@link File}, {@link Resource}, {@link InputStream} and Strings * starting with http:// or https://. * * @param args The argument to check. * @return A new list of arguments. */ private static Object[] processArgs(final Object[] args) { Object[] result = new Object[args.length]; for (int i = 0; i < args.length; i++) { EmbeddedType embeddedType = EmbeddedType.of(args[i]); result[i] = embeddedType.apply(embeddedType == EmbeddedType.DEFAULT ? args[i] : "embedded" + (i + 1)); } return result; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy