JavaMailBuilder.java
package fr.sii.ogham.email.builder.javamail;
import static fr.sii.ogham.core.condition.fluent.MessageConditions.requiredProperty;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.function.BiFunction;
import javax.activation.MimetypesFileTypeMap;
import javax.mail.Authenticator;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import fr.sii.ogham.core.builder.ActivableAtRuntime;
import fr.sii.ogham.core.builder.Builder;
import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.builder.configuration.ConfigurationValueBuilder;
import fr.sii.ogham.core.builder.configuration.ConfigurationValueBuilderHelper;
import fr.sii.ogham.core.builder.configurer.Configurer;
import fr.sii.ogham.core.builder.context.BuildContext;
import fr.sii.ogham.core.builder.context.DefaultBuildContext;
import fr.sii.ogham.core.builder.env.EnvironmentBuilder;
import fr.sii.ogham.core.builder.mimetype.MimetypeDetectionBuilder;
import fr.sii.ogham.core.builder.mimetype.MimetypeDetectionBuilderDelegate;
import fr.sii.ogham.core.builder.mimetype.SimpleMimetypeDetectionBuilder;
import fr.sii.ogham.core.charset.CharsetDetector;
import fr.sii.ogham.core.charset.FixedCharsetDetector;
import fr.sii.ogham.core.condition.Condition;
import fr.sii.ogham.core.convert.Converter;
import fr.sii.ogham.core.env.FirstExistingPropertiesResolver;
import fr.sii.ogham.core.env.JavaPropertiesResolver;
import fr.sii.ogham.core.env.PropertiesBridge;
import fr.sii.ogham.core.env.PropertyResolver;
import fr.sii.ogham.core.fluent.AbstractParent;
import fr.sii.ogham.core.message.Message;
import fr.sii.ogham.core.message.content.MayHaveStringContent;
import fr.sii.ogham.core.message.content.MultiContent;
import fr.sii.ogham.core.mimetype.MimeTypeProvider;
import fr.sii.ogham.core.resource.FileResource;
import fr.sii.ogham.core.resource.LookupResource;
import fr.sii.ogham.core.resource.NamedResource;
import fr.sii.ogham.core.resource.OverrideNameWrapper;
import fr.sii.ogham.email.attachment.Attachment;
import fr.sii.ogham.email.builder.EmailBuilder;
import fr.sii.ogham.email.exception.handler.AttachmentResourceHandlerException;
import fr.sii.ogham.email.exception.handler.UnresolvableAttachmentResourceHandlerException;
import fr.sii.ogham.email.message.Email;
import fr.sii.ogham.email.message.content.ContentWithAttachments;
import fr.sii.ogham.email.sender.impl.JavaMailSender;
import fr.sii.ogham.email.sender.impl.javamail.ContentWithAttachmentsHandler;
import fr.sii.ogham.email.sender.impl.javamail.FailResourceHandler;
import fr.sii.ogham.email.sender.impl.javamail.FileResourceHandler;
import fr.sii.ogham.email.sender.impl.javamail.JavaMailAttachmentHandler;
import fr.sii.ogham.email.sender.impl.javamail.JavaMailInterceptor;
import fr.sii.ogham.email.sender.impl.javamail.MapAttachmentResourceHandler;
import fr.sii.ogham.email.sender.impl.javamail.MultiContentHandler;
import fr.sii.ogham.email.sender.impl.javamail.OverrideNameWrapperResourceHandler;
import fr.sii.ogham.email.sender.impl.javamail.PriorizedContentHandler;
import fr.sii.ogham.email.sender.impl.javamail.StreamResourceHandler;
import fr.sii.ogham.email.sender.impl.javamail.StringContentHandler;
/**
* Configures how Java Mail implementation will send {@link Email}s.
*
* <p>
* To send {@link Email} using Java Mail, you need to register this builder into
* a {@link MessagingBuilder} like this:
*
* <pre>
* <code>
* MessagingBuilder msgBuilder = ...
* msgBuilder.email()
* .sender(JavaMailBuilder.class) // registers the builder and accesses to that builder for configuring it
* </code>
* </pre>
*
* Once the builder is registered, sending email through Java Mail requires at
* least host of the SMTP server. You can define it using:
*
* <pre>
* <code>
* msgBuilder.email()
* .sender(JavaMailBuilder.class) // registers the builder and accesses to that builder for configuring it
* .host("localhost")
* </code>
* </pre>
*
* Or you can also use property keys (using interpolation):
*
* <pre>
* <code>
* msgBuilder
* .environment()
* .properties()
* .set("custom.property.for.host", "localhost")
* .and()
* .and()
* .email()
* .sender(JavaMailBuilder.class) // registers the builder and accesses to that builder for configuring it
* .host()
* .properties("${custom.property.for.host}")
* </code>
* </pre>
*
* You can do the same with port of the SMTP server.
*
*
* <p>
* SMTP server may require authentication. In most cases, authentication is done
* using username/password. You can use this builder to quickly provide your
* username and password:
*
* <pre>
* <code>
* .sender(JavaMailBuilder.class)
* .authenticator()
* .username("foo")
* .password("bar")
* </code>
* </pre>
*
* If you need another authentication mechanism, you can directly provide your
* own {@link Authenticator} implementation:
*
* <pre>
* <code>
* .sender(JavaMailBuilder.class)
* .authenticator(new MyCustomAuthenticator())
* </code>
* </pre>
*
*
* <p>
* Finally, Ogham will transform general {@link Email} object into
* {@link MimeMessage}, {@link MimeMultipart}, {@link MimeBodyPart} objects.
* This transformation will fit almost all use cases but you may need to
* customize a part of the javax.mail message. Instead of doing again the same
* work Ogham does, this builder allows you to intercept the message to modify
* it just before sending it:
*
* <pre>
* <code>
* .sender(JavaMailBuilder.class)
* .intercept(new MyCustomInterceptor())
* </code>
* </pre>
*
* See {@link JavaMailInterceptor} for more information.
*
*
* @author Aurélien Baudet
*
*/
public class JavaMailBuilder extends AbstractParent<EmailBuilder> implements Builder<JavaMailSender>, ActivableAtRuntime {
private static final Logger LOG = LoggerFactory.getLogger(JavaMailBuilder.class);
private final BuildContext buildContext;
private final ConfigurationValueBuilderHelper<JavaMailBuilder, String> hostValueBuilder;
private final ConfigurationValueBuilderHelper<JavaMailBuilder, Integer> portValueBuilder;
private final ConfigurationValueBuilderHelper<JavaMailBuilder, Charset> charsetValueBuilder;
private final Properties additionalProperties;
private Authenticator authenticator;
private UsernamePasswordAuthenticatorBuilder authenticatorBuilder;
private JavaMailInterceptor interceptor;
private MimetypeDetectionBuilder<JavaMailBuilder> mimetypeBuilder;
private CharsetDetector charsetDetector;
/**
* Default constructor when using JavaMail sender without all Ogham work.
*
* <strong>WARNING: use is only if you know what you are doing !</strong>
*/
public JavaMailBuilder() {
this(null, new DefaultBuildContext());
mimetype();
}
/**
* Constructor that is called when using Ogham builder:
*
* <pre>
* MessagingBuilder msgBuilder = ...
* msgBuilder
* .email()
* .sender(JavaMailBuilder.class)
* </pre>
*
* <p>
* Initializes the builder with the parent instance (used by the
* {@link #and()} method) and the {@link EnvironmentBuilder}. The
* {@link EnvironmentBuilder} is used to evaluate property values when
* {@link #build()} is called.
*
* @param parent
* the parent builder instance for fluent chaining
* @param buildContext
* used to evaluate property values
*/
public JavaMailBuilder(EmailBuilder parent, BuildContext buildContext) {
super(parent);
this.buildContext = buildContext;
hostValueBuilder = buildContext.newConfigurationValueBuilder(this, String.class);
portValueBuilder = buildContext.newConfigurationValueBuilder(this, Integer.class);
charsetValueBuilder = buildContext.newConfigurationValueBuilder(this, Charset.class);
additionalProperties = new Properties();
}
/**
* Set the mail server address host (IP or hostname).
*
* <p>
* The value set using this method takes precedence over any property and
* default value configured using {@link #host()}.
*
* <pre>
* .host("smtp.gmail.com")
* .host()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue("localhost")
* </pre>
*
* <pre>
* .host("smtp.gmail.com")
* .host()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue("localhost")
* </pre>
*
* In both cases, {@code host("smtp.gmail.com")} is used.
*
* <p>
* If this method is called several times, only the last value is used.
*
* <p>
* If {@code null} value is set, it is like not setting a value at all. The
* property/default value configuration is applied.
*
* @param host
* the host of the mail server
* @return this instance for fluent chaining
*/
public JavaMailBuilder host(String host) {
hostValueBuilder.setValue(host);
return this;
}
/**
* Set the mail server address host (IP or hostname).
*
* <p>
* This method is mainly used by {@link Configurer}s to register some
* property keys and/or a default value. The aim is to let developer be able
* to externalize its configuration (using system properties, configuration
* file or anything else). If the developer doesn't configure any value for
* the registered properties, the default value is used (if set).
*
* <pre>
* .host()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue("localhost")
* </pre>
*
* <p>
* Non-null value set using {@link #host(String)} takes precedence over
* property values and default value.
*
* <pre>
* .host("smtp.gmail.com")
* .host()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue("localhost")
* </pre>
*
* The value {@code "smtp.gmail.com"} is used regardless of the value of the
* properties and default value.
*
* <p>
* See {@link ConfigurationValueBuilder} for more information.
*
*
* @return the builder to configure property keys/default value
*/
public ConfigurationValueBuilder<JavaMailBuilder, String> host() {
return hostValueBuilder;
}
/**
* Set the mail server port.
*
* <p>
* The value set using this method takes precedence over any property and
* default value configured using {@link #port()}.
*
* <pre>
* .port(10025)
* .port()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(25)
* </pre>
*
* <pre>
* .port(10025)
* .port()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(25)
* </pre>
*
* In both cases, {@code port(10025)} is used.
*
* <p>
* If this method is called several times, only the last value is used.
*
* <p>
* If {@code null} value is set, it is like not setting a value at all. The
* property/default value configuration is applied.
*
* @param port
* the port of mail server
* @return this instance for fluent chaining
*/
public JavaMailBuilder port(Integer port) {
portValueBuilder.setValue(port);
return this;
}
/**
* Set the mail server port
*
* <p>
* This method is mainly used by {@link Configurer}s to register some
* property keys and/or a default value. The aim is to let developer be able
* to externalize its configuration (using system properties, configuration
* file or anything else). If the developer doesn't configure any value for
* the registered properties, the default value is used (if set).
*
* <pre>
* .port()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(25)
* </pre>
*
* <p>
* Non-null value set using {@link #port(Integer)} takes precedence over
* property values and default value.
*
* <pre>
* .port(10025)
* .port()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(25)
* </pre>
*
* The value {@code 10025} is used regardless of the value of the properties
* and default value.
*
* <p>
* See {@link ConfigurationValueBuilder} for more information.
*
*
* @return the builder to configure property keys/default value
*/
public ConfigurationValueBuilder<JavaMailBuilder, Integer> port() {
return portValueBuilder;
}
/**
* Set charset to use for email body.
*
* <p>
* The value set using this method takes precedence over any property and
* default value configured using {@link #charset()}.
*
* <pre>
* .charset(StandardCharsets.UTF_16)
* .charset()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(StandardCharsets.UTF_8)
* </pre>
*
* <pre>
* .charset(StandardCharsets.UTF_16)
* .charset()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(StandardCharsets.UTF_8)
* </pre>
*
* In both cases, {@code charset(StandardCharsets.UTF_16)} is used.
*
* <p>
* If this method is called several times, only the last value is used.
*
* <p>
* If {@code null} value is set, it is like not setting a value at all. The
* property/default value configuration is applied.
*
* @param charset
* the charset to use for email body
* @return this instance for fluent chaining
*/
public JavaMailBuilder charset(Charset charset) {
charsetValueBuilder.setValue(charset);
return this;
}
/**
* Set charset to use for email body
*
* <p>
* This method is mainly used by {@link Configurer}s to register some
* property keys and/or a default value. The aim is to let developer be able
* to externalize its configuration (using system properties, configuration
* file or anything else). If the developer doesn't configure any value for
* the registered properties, the default value is used (if set).
*
* <pre>
* .charset()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(StandardCharsets.UTF_8)
* </pre>
*
* <p>
* Non-null value set using {@link #charset(Charset)} takes precedence over
* property values and default value.
*
* <pre>
* .charset(StandardCharsets.UTF_16)
* .charset()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(StandardCharsets.UTF_8)
* </pre>
*
* The value {@code StandardCharsets.UTF_16} is used regardless of the value
* of the properties and default value.
*
* <p>
* See {@link ConfigurationValueBuilder} for more information.
*
*
* @return the builder to configure property keys/default value
*/
public ConfigurationValueBuilder<JavaMailBuilder, Charset> charset() {
return charsetValueBuilder;
}
/**
* Defines a custom detector that will indicate which charset corresponds
* for a particular string.
*
* This value preempts any other value defined by calling
* {@link #charset(Charset)} method.
*
* If this method is called several times, only the last provider is used.
*
* @param charsetDetector
* the provider used to detect charset of a string
* @return this instance for fluent chaining
*/
public JavaMailBuilder charset(CharsetDetector charsetDetector) {
this.charsetDetector = charsetDetector;
return this;
}
/**
* SMTP server may require authentication. In most cases, authentication is
* done using username/password. You can use this builder to quickly provide
* your username and password:
*
* <pre>
* .sender(JavaMailBuilder.class)
* .authenticator()
* .username("foo")
* .password("bar")
* </pre>
*
* @return the builder to configure username/password authentication
*/
public UsernamePasswordAuthenticatorBuilder authenticator() {
if (authenticatorBuilder == null) {
authenticatorBuilder = new UsernamePasswordAuthenticatorBuilder(this, buildContext);
}
return authenticatorBuilder;
}
/**
* SMTP server may require authentication. In most cases, authentication is
* done using username/password. However, if you need another authentication
* mechanism, you can directly provide your own {@link Authenticator}
* implementation:
*
* <pre>
* .sender(JavaMailBuilder.class)
* .authenticator(new MyCustomAuthenticator())
* </pre>
*
* @param authenticator
* the custom authenticator implementation
* @return the builder to configure username/password authentication
*/
public JavaMailBuilder authenticator(Authenticator authenticator) {
this.authenticator = authenticator;
return this;
}
/**
* Ogham will transform general {@link Email} object into
* {@link MimeMessage}, {@link MimeMultipart}, {@link MimeBodyPart} objects.
* This transformation will fit almost all use cases but you may need to
* customize a part of the javax.mail message. Instead of doing again the
* same work Ogham does, this builder allows you to intercept the message to
* modify it just before sending it:
*
* <pre>
* .sender(JavaMailBuilder.class)
* .intercept(new MyCustomInterceptor())
* </pre>
*
* See {@link JavaMailInterceptor} for more information.
*
* @param interceptor
* the custom interceptor used to modify {@link MimeMessage}
* @return this instance for fluent chaining
*/
public JavaMailBuilder intercept(JavaMailInterceptor interceptor) {
this.interceptor = interceptor;
return this;
}
/**
* Builder that configures mimetype detection. Detection is used here to
* detect mimetype of {@link Attachment}s.
*
* There exists several implementations to provide the mimetype:
* <ul>
* <li>Using Java {@link MimetypesFileTypeMap}</li>
* <li>Using Java 7 {@link Files#probeContentType(java.nio.file.Path)}</li>
* <li>Using <a href="http://tika.apache.org/">Apache Tika</a></li>
* <li>Using
* <a href="https://github.com/arimus/jmimemagic">JMimeMagic</a></li>
* </ul>
*
* <p>
* Both implementations provided by Java are based on file extensions. This
* can't be used in most cases as we often handle {@link InputStream}s.
* </p>
*
* <p>
* In previous version of Ogham, JMimeMagic was used and was working quite
* well. Unfortunately, the library is no more maintained.
* </p>
*
* <p>
* You can configure how Tika will detect mimetype:
*
* <pre>
* .mimetype()
* .tika()
* ...
* </pre>
*
* <p>
* This builder allows to use several providers. It will chain them until
* one can find a valid mimetype. If none is found, you can explicitly
* provide the default one:
*
* <pre>
* .mimetype()
* .defaultMimetype("text/html")
* </pre>
*
* <p>
* If no mimetype detector was previously defined, it creates a new one.
* Then each time you call {@link #mimetype()}, the same instance is used.
* </p>
*
* @return the builder to configure mimetype detection
*/
public MimetypeDetectionBuilder<JavaMailBuilder> mimetype() {
if (mimetypeBuilder == null) {
mimetypeBuilder = new SimpleMimetypeDetectionBuilder<>(this, buildContext);
}
return mimetypeBuilder;
}
/**
* NOTE: this is mostly for advance usage (when creating a custom module).
*
* Inherits mimetype configuration from another builder. This is useful for
* configuring independently different parts of Ogham but keeping a whole
* coherence.
*
* The same instance is shared meaning that all changes done here will also
* impact the other builder.
*
* <p>
* If a previous builder was defined (by calling {@link #mimetype()} for
* example), the new builder will override it.
*
* @param builder
* the builder to inherit
* @return this instance for fluent chaining
*/
public JavaMailBuilder mimetype(MimetypeDetectionBuilder<?> builder) {
mimetypeBuilder = new MimetypeDetectionBuilderDelegate<>(this, builder);
return this;
}
/**
* Register additional properties that are used by JavaMail session.
*
* <p>
* If a key was previously registered, it is replaced by the new value.
*
* <p>
* If the value is {@code null}, the key is removed.
*
* @param props
* the properties to register
* @return this instance for fluent chaining
*/
public JavaMailBuilder properties(Properties props) {
updateProperties(props.entrySet());
return this;
}
/**
* Register additional properties that are used by JavaMail session.
*
* <p>
* If a key was previously registered, it is replaced by the new value.
*
* <p>
* If the value is {@code null}, the key is removed.
*
*
* @param props
* the properties to register
* @return this instance for fluent chaining
*/
public JavaMailBuilder properties(Map<String, String> props) {
updateProperties(props.entrySet());
return this;
}
@Override
public JavaMailSender build() {
Properties props = buildProperties();
MimeTypeProvider mimetypeProvider = mimetype().build();
LOG.info("Sending email using JavaMail API is registered");
LOG.debug("SMTP server address: {}:{}", props.getProperty("mail.host"), props.getProperty("mail.port"));
JavaMailAttachmentHandler attachmentHandler = buildAttachmentHandler(mimetypeProvider);
return buildContext.register(new JavaMailSender(props, buildContentHandler(mimetypeProvider, attachmentHandler), attachmentHandler, buildAuthenticator(), interceptor));
}
@Override
public Condition<Message> getCondition() {
PropertyResolver propertyResolver = buildPropertyResolver();
return requiredProperty(propertyResolver, "mail.host").or(requiredProperty(propertyResolver, "mail.smtp.host"));
}
private Properties buildProperties() {
return buildContext.register(new PropertiesBridge(new FirstExistingPropertiesResolver(buildPropertyResolver(), new JavaPropertiesResolver(additionalProperties, getConverter()))));
}
private OverrideJavaMailResolver buildPropertyResolver() {
return buildContext.register(new OverrideJavaMailResolver(getPropertyResolver(), getConverter(), hostValueBuilder, portValueBuilder));
}
private Converter getConverter() {
return buildContext.getConverter();
}
private Authenticator buildAuthenticator() {
if (this.authenticator != null) {
return this.authenticator;
}
if (authenticatorBuilder != null) {
return authenticatorBuilder.build();
}
return null;
}
private PriorizedContentHandler buildContentHandler(MimeTypeProvider mimetypeProvider, JavaMailAttachmentHandler attachmentHandler) {
PriorizedContentHandler contentHandler = buildContext.register(new PriorizedContentHandler());
contentHandler.register(MultiContent.class, buildContext.register(new MultiContentHandler(contentHandler)));
contentHandler.register(ContentWithAttachments.class, buildContext.register(new ContentWithAttachmentsHandler(contentHandler, attachmentHandler)));
contentHandler.register(MayHaveStringContent.class, buildContext.register(new StringContentHandler(mimetypeProvider, buildCharset())));
return contentHandler;
}
private CharsetDetector buildCharset() {
if (this.charsetDetector != null) {
return this.charsetDetector;
}
Charset charset = this.charsetValueBuilder.getValue();
if (charset != null) {
return buildContext.register(new FixedCharsetDetector(charset));
}
return buildContext.register(new FixedCharsetDetector());
}
private PropertyResolver getPropertyResolver() {
return buildContext.getPropertyResolver();
}
private JavaMailAttachmentHandler buildAttachmentHandler(MimeTypeProvider mimetypeProvider) {
return buildContext.register(new JavaMailAttachmentHandler(buildAttachmentResourceHandler(mimetypeProvider)));
}
private MapAttachmentResourceHandler buildAttachmentResourceHandler(MimeTypeProvider mimetypeProvider) {
MapAttachmentResourceHandler resourceHandler = buildContext.register(new MapAttachmentResourceHandler());
resourceHandler.registerResourceHandler(FileResource.class, buildContext.register(new FileResourceHandler(mimetypeProvider)));
resourceHandler.registerResourceHandler(OverrideNameWrapper.class, buildContext.register(new OverrideNameWrapperResourceHandler(resourceHandler)));
resourceHandler.registerResourceHandler(LookupResource.class, buildContext.register(new FailResourceHandler(noResourceResolverConfigured())));
resourceHandler.registerResourceHandler(NamedResource.class, buildContext.register(new StreamResourceHandler(mimetypeProvider)));
return resourceHandler;
}
private static BiFunction<NamedResource, Attachment, AttachmentResourceHandlerException> noResourceResolverConfigured() {
return (resource, attachment) -> new UnresolvableAttachmentResourceHandlerException(
"Failed to attach " + resource.getName() + " because it points to a path but no resource resolver has been configured.", attachment);
}
private void updateProperties(Set<? extends Entry<?, ?>> entrySet) {
for (Entry<?, ?> prop : entrySet) {
if (prop.getValue() == null) {
additionalProperties.remove(prop.getKey());
} else {
additionalProperties.setProperty(prop.getKey().toString(), prop.getValue().toString());
}
}
}
}