Skip to content

Commit 20c2158

Browse files
committed
#260: dynamic datasource resolution for html embedded images
1 parent 49f975b commit 20c2158

File tree

7 files changed

+323
-4
lines changed

7 files changed

+323
-4
lines changed

modules/core-module/src/main/java/org/simplejavamail/api/email/EmailPopulatingBuilder.java

+16
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
import java.util.Date;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.regex.Pattern;
24+
25+
import static java.util.regex.Pattern.compile;
2326

2427
/**
2528
* Fluent interface Builder for populating {@link Email} instances. An instance of this builder can only be obtained through one of the builder
@@ -30,6 +33,13 @@
3033
*/
3134
@Cli.BuilderApiNode(builderApiType = CliBuilderApiType.EMAIL)
3235
public interface EmailPopulatingBuilder {
36+
37+
/**
38+
* Regular Expression to find all {@code <img src="...">} entries in an HTML document.It needs to cater for various things, like more whitespaces including newlines on any place, HTML is not case
39+
* sensitive and there can be arbitrary text between "IMG" and "SRC" like IDs and other things.
40+
*/
41+
Pattern IMG_SRC_PATTERN = compile("(?<imageTagStart><[Ii][Mm][Gg]\\s*[^>]*?\\s+[Ss][Rr][Cc]\\s*=\\s*[\"'])(?<src>[^\"']+?)(?<imageSrcEnd>[\"'])");
42+
3343
/**
3444
* Validated DKIM values and then delegates to {@link Email#Email(EmailPopulatingBuilder)} with <code>this</code> as argument.
3545
*/
@@ -857,20 +867,26 @@ public interface EmailPopulatingBuilder {
857867
* Sets the base folder used when resolving images sources in HTML text. Without this, the folder needs to be an absolute path (or a classpath/url resource).
858868
* <p>
859869
* Generally you would manually use src="cid:image_name", but files and url's will be located as well dynamically.
870+
*
871+
* @param embeddedImageBaseDir The base folder used when resolving images sources in HTML text.
860872
*/
861873
EmailPopulatingBuilder withEmbeddedImageBaseDir(@NotNull final String embeddedImageBaseDir);
862874

863875
/**
864876
* Sets the classpath base used when resolving images sources in HTML text. Without this, the resource needs to be an absolute path (or a file/url resource).
865877
* <p>
866878
* Generally you would manually use src="cid:image_name", but files and url's will be located as well dynamically.
879+
*
880+
* @param embeddedImageBaseClassPath The classpath base used when resolving images sources in HTML text.
867881
*/
868882
EmailPopulatingBuilder withEmbeddedImageBaseClassPath(@NotNull final String embeddedImageBaseClassPath);
869883

870884
/**
871885
* Sets the base URL used when resolving images sources in HTML text. Without this, the resource needs to be an absolute URL (or a file/classpath resource).
872886
* <p>
873887
* Generally you would manually use src="cid:image_name", but files and url's will be located as well dynamically.
888+
*
889+
* @param embeddedImageBaseUrl The base URL used when resolving images sources in HTML text.
874890
*/
875891
EmailPopulatingBuilder withEmbeddedImageBaseUrl(@NotNull final URL embeddedImageBaseUrl);
876892

modules/core-module/src/main/java/org/simplejavamail/config/ConfigLoader.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@
7070
* <li>simplejavamail.smime.signing.key_alias</li>
7171
* <li>simplejavamail.smime.signing.key_password</li>
7272
* <li>simplejavamail.smime.encryption.certificate</li>
73+
* <li>simplejavamail.embeddedimages.dynamicresolution.base.dir</li>
74+
* <li>simplejavamail.embeddedimages.dynamicresolution.base.url</li>
75+
* <li>simplejavamail.embeddedimages.dynamicresolution.base.classpath</li>
7376
* </ul>
7477
*/
7578
public final class ConfigLoader {
@@ -144,7 +147,10 @@ public enum Property {
144147
SMIME_SIGNING_KEYSTORE_PASSWORD("simplejavamail.smime.signing.keystore_password"),
145148
SMIME_SIGNING_KEY_ALIAS("simplejavamail.smime.signing.key_alias"),
146149
SMIME_SIGNING_KEY_PASSWORD("simplejavamail.smime.signing.key_password"),
147-
SMIME_ENCRYPTION_CERTIFICATE("simplejavamail.smime.encryption.certificate");
150+
SMIME_ENCRYPTION_CERTIFICATE("simplejavamail.smime.encryption.certificate"),
151+
EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_DIR("simplejavamail.embeddedimages.dynamicresolution.base.dir"),
152+
EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_URL("simplejavamail.embeddedimages.dynamicresolution.base.url"),
153+
EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_CLASSPATH("simplejavamail.embeddedimages.dynamicresolution.base.classpath");
148154

149155
private final String key;
150156

modules/core-module/src/main/java/org/simplejavamail/internal/util/MiscUtil.java

+81
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@
55
import org.jetbrains.annotations.Nullable;
66
import org.simplejavamail.api.email.Recipient;
77

8+
import javax.activation.DataSource;
9+
import javax.activation.FileDataSource;
10+
import javax.activation.FileTypeMap;
11+
import javax.activation.URLDataSource;
812
import javax.mail.Message.RecipientType;
913
import javax.mail.internet.AddressException;
1014
import javax.mail.internet.InternetAddress;
1115
import javax.mail.internet.MimeUtility;
16+
import javax.mail.util.ByteArrayDataSource;
1217
import java.io.BufferedInputStream;
1318
import java.io.ByteArrayInputStream;
1419
import java.io.ByteArrayOutputStream;
@@ -19,22 +24,26 @@
1924
import java.io.UnsupportedEncodingException;
2025
import java.lang.annotation.Annotation;
2126
import java.lang.reflect.Method;
27+
import java.net.URL;
2228
import java.nio.charset.Charset;
2329
import java.nio.file.Files;
2430
import java.util.AbstractMap;
2531
import java.util.ArrayList;
2632
import java.util.Collection;
2733
import java.util.List;
2834
import java.util.Map;
35+
import java.util.Random;
2936
import java.util.regex.Pattern;
3037

3138
import static java.lang.Integer.toHexString;
3239
import static java.lang.String.format;
3340
import static java.nio.charset.StandardCharsets.UTF_8;
3441
import static java.util.Arrays.asList;
3542
import static java.util.regex.Pattern.compile;
43+
import static java.util.regex.Pattern.quote;
3644
import static org.simplejavamail.internal.util.Preconditions.assumeTrue;
3745
import static org.simplejavamail.internal.util.Preconditions.checkNonEmptyArgument;
46+
import static org.simplejavamail.internal.util.SimpleOptional.ofNullable;
3847

3948
public final class MiscUtil {
4049

@@ -44,6 +53,10 @@ public final class MiscUtil {
4453
private static final Pattern TRAILING_TOKEN_DELIMITER_PATTERN = compile("<\\|>$");
4554
private static final Pattern TOKEN_DELIMITER_PATTERN = compile("\\s*<\\|>\\s*");
4655

56+
private static final Pattern ABSOLUTE_URL_PATTERN = compile(format("^(%s|%s|%s).*", quote("http://"), quote("https://"), quote("file:/")));
57+
58+
private static final Random RANDOM = new Random();
59+
4760
@SuppressFBWarnings(value = "NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE")
4861
public static <T> T checkNotNull(final @Nullable T value, final @Nullable String msg) {
4962
if (value == null) {
@@ -263,4 +276,72 @@ public static ByteArrayInputStream copyInputstream(InputStream input) {
263276

264277
return new ByteArrayInputStream(baos.toByteArray());
265278
}
279+
280+
@Nullable
281+
public static DataSource tryResolveFileDataSource(@Nullable final String baseDir, @Nullable final String baseClassPath, @NotNull final String srcLocation)
282+
throws IOException {
283+
DataSource fileSource = tryResolveFileDataSourceFromDisk(baseDir, srcLocation);
284+
return (fileSource != null) ? fileSource : tryResolveFileDataSourceFromClassPath(baseClassPath, srcLocation);
285+
}
286+
287+
@Nullable
288+
private static DataSource tryResolveFileDataSourceFromDisk(final @Nullable String baseDir, final @NotNull String srcLocation) {
289+
File file = new File(srcLocation);
290+
if (!file.exists() && !file.isAbsolute()) {
291+
file = new File(ofNullable(baseDir).orElse("."), srcLocation);
292+
}
293+
if (file.exists()) {
294+
return new FileDataSource(file);
295+
}
296+
return null;
297+
}
298+
299+
@Nullable
300+
private static DataSource tryResolveFileDataSourceFromClassPath(final @Nullable String baseClassPath, final @NotNull String srcLocation)
301+
throws IOException {
302+
final String resourceName = (ofNullable(baseClassPath).orElse("") + srcLocation).replaceAll("//", "/");
303+
final InputStream is = MiscUtil.class.getResourceAsStream(resourceName);
304+
305+
if (is != null) {
306+
try {
307+
final String mimeType = FileTypeMap.getDefaultFileTypeMap().getContentType(srcLocation);
308+
final ByteArrayDataSource ds = new ByteArrayDataSource(is, mimeType);
309+
// EMAIL-125: set the name of the DataSource to the normalized resource URL similar to other DataSource implementations, e.g. FileDataSource, URLDataSource
310+
ds.setName(MiscUtil.class.getResource(resourceName).toString());
311+
return ds;
312+
} finally {
313+
is.close();
314+
}
315+
}
316+
return null;
317+
}
318+
319+
@NotNull
320+
public static DataSource resolveUrlDataSource(@Nullable final URL baseUrl, @NotNull final String srcLocation)
321+
throws IOException {
322+
final URL url = (valueNullOrEmpty(baseUrl) || ABSOLUTE_URL_PATTERN.matcher(srcLocation).matches())
323+
? new URL(srcLocation)
324+
: new URL(baseUrl, srcLocation.replaceAll("&amp;", "&"));
325+
326+
DataSource result = new URLDataSource(url);
327+
result.getInputStream();
328+
return result;
329+
}
330+
331+
public static String randomCid10() {
332+
final int start = ' ';
333+
final int end = 'z' + 1;
334+
final int gap = end - start;
335+
336+
final StringBuilder buffer = new StringBuilder();
337+
338+
while (buffer.length() < 10) {
339+
final char ch = (char) (RANDOM.nextInt(gap) + start);
340+
if (Character.isLetter(ch)) {
341+
buffer.append(ch);
342+
}
343+
}
344+
345+
return buffer.toString().toLowerCase();
346+
}
266347
}

modules/simple-java-mail/src/main/java/org/simplejavamail/email/internal/EmailException.java

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class EmailException extends MailException {
1212
static final String ERROR_READING_FROM_FILE = "Error reading from file: %s";
1313
static final String ERROR_READING_FROM_PEM_INPUTSTREAM = "Was unable to convert PEM data to X509 certificate";
1414
static final String ERROR_LOADING_PROVIDER_FOR_SMIME_SUPPORT = "Unable to load certificate (missing bouncy castle), is the S/MIME module on the class path?";
15+
static final String ERROR_RESOLVING_IMAGE_DATASOURCE = "Unable to dynamically resolve data source for the following image src: %s";
1516

1617
EmailException(@SuppressWarnings("SameParameterValue") final String message) {
1718
super(message);

modules/simple-java-mail/src/main/java/org/simplejavamail/email/internal/EmailPopulatingBuilderImpl.java

+54
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@
3838
import java.util.HashMap;
3939
import java.util.List;
4040
import java.util.Map;
41+
import java.util.regex.Matcher;
4142

4243
import static java.lang.String.format;
4344
import static java.nio.charset.StandardCharsets.UTF_8;
4445
import static java.util.Arrays.asList;
4546
import static java.util.Collections.singletonList;
47+
import static java.util.regex.Matcher.quoteReplacement;
4648
import static javax.mail.Message.RecipientType.BCC;
4749
import static javax.mail.Message.RecipientType.CC;
4850
import static javax.mail.Message.RecipientType.TO;
@@ -59,6 +61,9 @@
5961
import static org.simplejavamail.config.ConfigLoader.Property.DEFAULT_SUBJECT;
6062
import static org.simplejavamail.config.ConfigLoader.Property.DEFAULT_TO_ADDRESS;
6163
import static org.simplejavamail.config.ConfigLoader.Property.DEFAULT_TO_NAME;
64+
import static org.simplejavamail.config.ConfigLoader.Property.EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_CLASSPATH;
65+
import static org.simplejavamail.config.ConfigLoader.Property.EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_DIR;
66+
import static org.simplejavamail.config.ConfigLoader.Property.EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_URL;
6267
import static org.simplejavamail.config.ConfigLoader.Property.SMIME_ENCRYPTION_CERTIFICATE;
6368
import static org.simplejavamail.config.ConfigLoader.Property.SMIME_SIGNING_KEYSTORE;
6469
import static org.simplejavamail.config.ConfigLoader.Property.SMIME_SIGNING_KEYSTORE_PASSWORD;
@@ -70,10 +75,14 @@
7075
import static org.simplejavamail.email.internal.EmailException.ERROR_LOADING_PROVIDER_FOR_SMIME_SUPPORT;
7176
import static org.simplejavamail.email.internal.EmailException.ERROR_READING_FROM_FILE;
7277
import static org.simplejavamail.email.internal.EmailException.ERROR_READING_FROM_PEM_INPUTSTREAM;
78+
import static org.simplejavamail.email.internal.EmailException.ERROR_RESOLVING_IMAGE_DATASOURCE;
7379
import static org.simplejavamail.email.internal.EmailException.NAME_MISSING_FOR_EMBEDDED_IMAGE;
7480
import static org.simplejavamail.internal.smimesupport.SmimeRecognitionUtil.isGeneratedSmimeMessageId;
7581
import static org.simplejavamail.internal.util.MiscUtil.defaultTo;
7682
import static org.simplejavamail.internal.util.MiscUtil.extractEmailAddresses;
83+
import static org.simplejavamail.internal.util.MiscUtil.randomCid10;
84+
import static org.simplejavamail.internal.util.MiscUtil.resolveUrlDataSource;
85+
import static org.simplejavamail.internal.util.MiscUtil.tryResolveFileDataSource;
7786
import static org.simplejavamail.internal.util.MiscUtil.valueNullOrEmpty;
7887
import static org.simplejavamail.internal.util.Preconditions.assumeNonNull;
7988
import static org.simplejavamail.internal.util.Preconditions.checkNonEmptyArgument;
@@ -322,6 +331,15 @@ public class EmailPopulatingBuilderImpl implements InternalEmailPopulatingBuilde
322331
if (hasProperty(SMIME_ENCRYPTION_CERTIFICATE)) {
323332
encryptWithSmime(assumeNonNull(getStringProperty(SMIME_ENCRYPTION_CERTIFICATE)));
324333
}
334+
if (hasProperty(EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_DIR)) {
335+
withEmbeddedImageBaseDir(assumeNonNull(getStringProperty(EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_DIR)));
336+
}
337+
if (hasProperty(EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_URL)) {
338+
withEmbeddedImageBaseDir(assumeNonNull(getStringProperty(EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_URL)));
339+
}
340+
if (hasProperty(EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_CLASSPATH)) {
341+
withEmbeddedImageBaseDir(assumeNonNull(getStringProperty(EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_CLASSPATH)));
342+
}
325343
}
326344
}
327345

@@ -332,6 +350,7 @@ public class EmailPopulatingBuilderImpl implements InternalEmailPopulatingBuilde
332350
@Cli.ExcludeApi(reason = "This API is specifically for Java use")
333351
public Email buildEmail() {
334352
validateDkim();
353+
resolveDynamicEmbeddedImageDataSources();
335354
return new Email(this);
336355
}
337356

@@ -343,6 +362,41 @@ private void validateDkim() {
343362
}
344363
}
345364

365+
private void resolveDynamicEmbeddedImageDataSources() {
366+
if (this.textHTML != null) {
367+
final Map<String, String> generatedCids = new HashMap<>();
368+
final StringBuffer stringBuffer = new StringBuffer();
369+
370+
final Matcher matcher = IMG_SRC_PATTERN.matcher(this.textHTML);
371+
while (matcher.find()) {
372+
final String srcLocation = matcher.group("src");
373+
if (!srcLocation.startsWith("cid:")) {
374+
if (!generatedCids.containsKey(srcLocation)) {
375+
generatedCids.put(srcLocation, randomCid10());
376+
withEmbeddedImage(generatedCids.get(srcLocation), resolveDynamicEmbeddedImageDataSource(srcLocation));
377+
}
378+
final String imgSrcReplacement = matcher.group("imageTagStart") + "cid:" + generatedCids.get(srcLocation) + matcher.group("imageSrcEnd");
379+
matcher.appendReplacement(stringBuffer, quoteReplacement(imgSrcReplacement));
380+
}
381+
}
382+
matcher.appendTail(stringBuffer);
383+
384+
this.textHTML = stringBuffer.toString();
385+
}
386+
}
387+
388+
private DataSource resolveDynamicEmbeddedImageDataSource(@NotNull final String srcLocation) {
389+
try {
390+
DataSource resolvedDataSource = tryResolveFileDataSource(embeddedImageBaseDir, embeddedImageBaseClassPath, srcLocation);
391+
if (resolvedDataSource == null) {
392+
resolvedDataSource = resolveUrlDataSource(embeddedImageBaseUrl, srcLocation);
393+
}
394+
return resolvedDataSource;
395+
} catch (IOException e) {
396+
throw new EmailException(format(ERROR_RESOLVING_IMAGE_DATASOURCE, srcLocation));
397+
}
398+
}
399+
346400
/**
347401
* @see EmailPopulatingBuilder#fixingMessageId(String)
348402
*/

0 commit comments

Comments
 (0)