Skip to content

Commit 416399f

Browse files
committed
#260: Changed default behavior to not auto-resolve, added api to enable it, added api to force resolving only from basepath or allow outside, added option to enforce resolution or fail with exception that image source could not be resolved
1 parent 16e70e6 commit 416399f

File tree

10 files changed

+777
-63
lines changed

10 files changed

+777
-63
lines changed

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,12 +863,58 @@ public interface EmailPopulatingBuilder {
863863
*/
864864
EmailPopulatingBuilder withRecipient(@NotNull Recipient recipient);
865865

866+
/**
867+
* Enables auto resolution of file datasources for embedded images.
868+
* <p>
869+
* Normally, you would manually markup your HTML with images using {@code cid:<some_id>} and then add an embedded image
870+
* resource with the same name ({@code emailBuilder.withEmbeddedImage(..)}). With auto-file-resolution, you can just
871+
* refer to the file instead and the data will be included dynamically with a generated <em>cid</em>.
872+
*
873+
* @param embeddedImageAutoResolutionForFiles Enables auto resolution of file datasources for embedded images.
874+
*
875+
* @see #withEmbeddedImageBaseDir(String)
876+
* @see #allowingEmbeddedImageOutsideBaseDir(boolean)
877+
*/
878+
EmailPopulatingBuilder withEmbeddedImageAutoResolutionForFiles(final boolean embeddedImageAutoResolutionForFiles);
879+
880+
/**
881+
* Enables auto resolution of classpath datasources for embedded images.
882+
* <p>
883+
* Normally, you would manually markup your HTML with images using {@code cid:<some_id>} and then add an embedded image
884+
* resource with the same name ({@code emailBuilder.withEmbeddedImage(..)}). With auto-classpath-resolution, you can just
885+
* refer to the resource on the classpath instead and the data will be included dynamically with a generated <em>cid</em>.
886+
*
887+
* @param embeddedImageAutoResolutionForClassPathResources Enables auto resolution of classpath datasources for embedded images.
888+
*
889+
* @see #withEmbeddedImageBaseClassPath(String)
890+
* @see #allowingEmbeddedImageOutsideBaseClassPath(boolean)
891+
*/
892+
EmailPopulatingBuilder withEmbeddedImageAutoResolutionForClassPathResources(final boolean embeddedImageAutoResolutionForClassPathResources);
893+
894+
/**
895+
* Enables auto resolution of URL's for embedded images.
896+
* <p>
897+
* Normally, you would manually markup your HTML with images using {@code cid:<some_id>} and then add an embedded image
898+
* resource with the same name ({@code emailBuilder.withEmbeddedImage(..)}). With auto-URL-resolution, you can just
899+
* refer to the hosted image instead and the data will be downloaded and included dynamically with a generated <em>cid</em>.
900+
*
901+
* @param embeddedImageAutoResolutionForURLs Enables auto resolution of URL's for embedded images.
902+
*
903+
* @see #withEmbeddedImageBaseUrl(String)
904+
* @see #withEmbeddedImageBaseUrl(URL)
905+
* @see #allowingEmbeddedImageOutsideBaseUrl(boolean)
906+
*/
907+
EmailPopulatingBuilder withEmbeddedImageAutoResolutionForURLs(final boolean embeddedImageAutoResolutionForURLs);
908+
866909
/**
867910
* 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).
868911
* <p>
869912
* Generally you would manually use src="cid:image_name", but files and url's will be located as well dynamically.
870913
*
871914
* @param embeddedImageBaseDir The base folder used when resolving images sources in HTML text.
915+
*
916+
* @see #withEmbeddedImageAutoResolutionForFiles(boolean)
917+
* @see #allowingEmbeddedImageOutsideBaseDir(boolean)
872918
*/
873919
EmailPopulatingBuilder withEmbeddedImageBaseDir(@NotNull final String embeddedImageBaseDir);
874920

@@ -878,13 +924,19 @@ public interface EmailPopulatingBuilder {
878924
* Generally you would manually use src="cid:image_name", but files and url's will be located as well dynamically.
879925
*
880926
* @param embeddedImageBaseClassPath The classpath base used when resolving images sources in HTML text.
927+
*
928+
* @see #withEmbeddedImageAutoResolutionForClassPathResources(boolean)
929+
* @see #allowingEmbeddedImageOutsideBaseClassPath(boolean)
881930
*/
882931
EmailPopulatingBuilder withEmbeddedImageBaseClassPath(@NotNull final String embeddedImageBaseClassPath);
883932

884933
/**
885934
* Delegates to {@link #withEmbeddedImageBaseUrl(URL)}.
886935
*
887936
* @param embeddedImageBaseUrl The base URL used when resolving images sources in HTML text.
937+
*
938+
* @see #withEmbeddedImageAutoResolutionForURLs(boolean)
939+
* @see #allowingEmbeddedImageOutsideBaseUrl(boolean)
888940
*/
889941
EmailPopulatingBuilder withEmbeddedImageBaseUrl(@NotNull final String embeddedImageBaseUrl);
890942

@@ -894,8 +946,55 @@ public interface EmailPopulatingBuilder {
894946
* Generally you would manually use src="cid:image_name", but files and url's will be located as well dynamically.
895947
*
896948
* @param embeddedImageBaseUrl The base URL used when resolving images sources in HTML text.
949+
*
950+
* @see #withEmbeddedImageAutoResolutionForURLs(boolean)
951+
* @see #allowingEmbeddedImageOutsideBaseUrl(boolean)
897952
*/
953+
@Cli.ExcludeApi(reason = "delegated method is an identical api from CLI point of view")
898954
EmailPopulatingBuilder withEmbeddedImageBaseUrl(@NotNull final URL embeddedImageBaseUrl);
955+
956+
/**
957+
* Dictates whether files will be resolved for embedded images when they are not nested under the baseDir (if baseDir is set).
958+
*
959+
* @param allowEmbeddedImageOutsideBaseDir Whether files should be resolved that reside outside of the baseDir (if set)
960+
*
961+
* @see #withEmbeddedImageAutoResolutionForFiles(boolean)
962+
* @see #withEmbeddedImageBaseDir(String)
963+
*/
964+
EmailPopulatingBuilder allowingEmbeddedImageOutsideBaseDir(final boolean allowEmbeddedImageOutsideBaseDir);
965+
966+
/**
967+
* Dictates whether sources will be resolved for embedded images when they are not nested under the baseClassPath (if baseClassPath is set).
968+
*
969+
* @param allowEmbeddedImageOutsideBaseClassPath Whether image sources should be resolved that reside outside of the baseClassPath (if set)
970+
*
971+
* @see #withEmbeddedImageAutoResolutionForClassPathResources(boolean)
972+
* @see #withEmbeddedImageBaseClassPath(String)
973+
*/
974+
EmailPopulatingBuilder allowingEmbeddedImageOutsideBaseClassPath(final boolean allowEmbeddedImageOutsideBaseClassPath);
975+
976+
/**
977+
* Dictates whether url's will be resolved for embedded images when they are not nested under the baseUrl (if baseUrl is set).
978+
*
979+
* @param allowEmbeddedImageOutsideBaseUrl Whether url's should be resolved that reside outside of the baseUrl (if set)
980+
*
981+
* @see #withEmbeddedImageAutoResolutionForURLs(boolean)
982+
* @see #withEmbeddedImageBaseUrl(String)
983+
* @see #withEmbeddedImageBaseUrl(URL)
984+
*/
985+
EmailPopulatingBuilder allowingEmbeddedImageOutsideBaseUrl(final boolean allowEmbeddedImageOutsideBaseUrl);
986+
987+
/**
988+
* When embedded image auto resolution is enabled, this option will make sure unresolved images sources result in an exception.
989+
* <p>
990+
* Not using this option effectively means a more lenient approach to image sources.
991+
* <p>
992+
* Note: It also allows you to work with URL's as image sources that can't be resolved at time of sending, but that makes sense
993+
* when viewing the email in some client (eg. relative url's).
994+
*
995+
* @param embeddedImageAutoResolutionMustBeSuccesful Whether auto resolution is enforced and bubbles up failure to do so.
996+
*/
997+
EmailPopulatingBuilder embeddedImageAutoResolutionMustBeSuccesful(final boolean embeddedImageAutoResolutionMustBeSuccesful);
899998

900999
/**
9011000
* Delegates to {@link #withEmbeddedImage(String, DataSource)}, with a named {@link ByteArrayDataSource} created using the provided name, data and

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,16 @@
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.enable.dir</li>
74+
* <li>simplejavamail.embeddedimages.dynamicresolution.enable.url</li>
75+
* <li>simplejavamail.embeddedimages.dynamicresolution.enable.classpath</li>
7376
* <li>simplejavamail.embeddedimages.dynamicresolution.base.dir</li>
7477
* <li>simplejavamail.embeddedimages.dynamicresolution.base.url</li>
7578
* <li>simplejavamail.embeddedimages.dynamicresolution.base.classpath</li>
79+
* <li>simplejavamail.embeddedimages.dynamicresolution.outside.base.dir</li>
80+
* <li>simplejavamail.embeddedimages.dynamicresolution.outside.base.classpath</li>
81+
* <li>simplejavamail.embeddedimages.dynamicresolution.outside.base.url</li>
82+
* <li>simplejavamail.embeddedimages.dynamicresolution.mustbesuccesful</li>
7683
* </ul>
7784
*/
7885
public final class ConfigLoader {
@@ -148,9 +155,16 @@ public enum Property {
148155
SMIME_SIGNING_KEY_ALIAS("simplejavamail.smime.signing.key_alias"),
149156
SMIME_SIGNING_KEY_PASSWORD("simplejavamail.smime.signing.key_password"),
150157
SMIME_ENCRYPTION_CERTIFICATE("simplejavamail.smime.encryption.certificate"),
158+
EMBEDDEDIMAGES_DYNAMICRESOLUTION_ENABLE_DIR("simplejavamail.embeddedimages.dynamicresolution.enable.dir"),
159+
EMBEDDEDIMAGES_DYNAMICRESOLUTION_ENABLE_CLASSPATH("simplejavamail.embeddedimages.dynamicresolution.enable.classpath"),
160+
EMBEDDEDIMAGES_DYNAMICRESOLUTION_ENABLE_URL("simplejavamail.embeddedimages.dynamicresolution.enable.url"),
151161
EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_DIR("simplejavamail.embeddedimages.dynamicresolution.base.dir"),
162+
EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_CLASSPATH("simplejavamail.embeddedimages.dynamicresolution.base.classpath"),
152163
EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_URL("simplejavamail.embeddedimages.dynamicresolution.base.url"),
153-
EMBEDDEDIMAGES_DYNAMICRESOLUTION_BASE_CLASSPATH("simplejavamail.embeddedimages.dynamicresolution.base.classpath");
164+
EMBEDDEDIMAGES_DYNAMICRESOLUTION_OUTSIDE_BASE_DIR("simplejavamail.embeddedimages.dynamicresolution.outside.base.dir"),
165+
EMBEDDEDIMAGES_DYNAMICRESOLUTION_OUTSIDE_BASE_URL("simplejavamail.embeddedimages.dynamicresolution.outside.base.classpath"),
166+
EMBEDDEDIMAGES_DYNAMICRESOLUTION_OUTSIDE_BASE_CLASSPATH("simplejavamail.embeddedimages.dynamicresolution.outside.base.url"),
167+
EMBEDDEDIMAGES_DYNAMICRESOLUTION_MUSTBESUCCESFUL("simplejavamail.embeddedimages.dynamicresolution.mustbesuccesful");
154168

155169
private final String key;
156170

@@ -235,12 +249,17 @@ public static synchronized <T> T getProperty(final Property property) {
235249
public static synchronized String getStringProperty(final Property property) {
236250
return SimpleConversions.convertToString(RESOLVED_PROPERTIES.get(property));
237251
}
238-
252+
239253
@Nullable
240254
public static synchronized Integer getIntegerProperty(final Property property) {
241255
return SimpleConversions.convertToInteger(RESOLVED_PROPERTIES.get(property));
242256
}
243257

258+
@Nullable
259+
public static synchronized Boolean getBooleanProperty(final Property property) {
260+
return SimpleConversions.convertToBoolean(RESOLVED_PROPERTIES.get(property));
261+
}
262+
244263
/**
245264
* Loads properties from property file on the classpath, if provided. Calling this method only has effect on new Email and Mailer instances after
246265
* this.

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

Lines changed: 90 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,8 @@
3939
import static java.nio.charset.StandardCharsets.UTF_8;
4040
import static java.util.Arrays.asList;
4141
import static java.util.regex.Pattern.compile;
42-
import static java.util.regex.Pattern.quote;
4342
import static org.simplejavamail.internal.util.Preconditions.assumeTrue;
4443
import static org.simplejavamail.internal.util.Preconditions.checkNonEmptyArgument;
45-
import static org.simplejavamail.internal.util.SimpleOptional.ofNullable;
4644

4745
public final class MiscUtil {
4846

@@ -52,8 +50,6 @@ public final class MiscUtil {
5250
private static final Pattern TRAILING_TOKEN_DELIMITER_PATTERN = compile("<\\|>$");
5351
private static final Pattern TOKEN_DELIMITER_PATTERN = compile("\\s*<\\|>\\s*");
5452

55-
private static final Pattern ABSOLUTE_URL_PATTERN = compile(format("^(%s|%s|%s).*", quote("http://"), quote("https://"), quote("file:/")));
56-
5753
private static final Random RANDOM = new Random();
5854

5955
@SuppressFBWarnings(value = "NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE")
@@ -277,38 +273,97 @@ public static ByteArrayInputStream copyInputstream(InputStream input) {
277273
}
278274

279275
@Nullable
280-
public static DataSource tryResolveImageFileDataSource(@Nullable final String baseDir, @Nullable final String baseClassPath, @NotNull final String srcLocation)
276+
public static DataSource tryResolveImageFileDataSourceFromDisk(final @Nullable String baseDir, final boolean allowOutsideBaseDir, final @NotNull String srcLocation) {
277+
DataSource dataSource;
278+
279+
if (baseDir == null) {
280+
dataSource = tryLoadingFromDisk(new File(srcLocation));
281+
if (dataSource == null) {
282+
dataSource = tryLoadingFromDisk(new File(".", srcLocation));
283+
}
284+
} else {
285+
if (srcLocation.startsWith(baseDir)) {
286+
dataSource = tryLoadingFromDisk(new File(srcLocation));
287+
} else {
288+
dataSource = tryLoadingFromDisk(new File(baseDir, srcLocation));
289+
if (dataSource == null && allowOutsideBaseDir) {
290+
dataSource = tryLoadingFromDisk(new File(".", srcLocation));
291+
if (dataSource == null) {
292+
dataSource = tryLoadingFromDisk(new File(srcLocation));
293+
}
294+
}
295+
}
296+
}
297+
return dataSource;
298+
}
299+
300+
@Nullable
301+
public static DataSource tryResolveFileDataSourceFromClassPath(final @Nullable String baseClassPath, final boolean allowOutsideBaseClassPath, final @NotNull String srcLocation)
281302
throws IOException {
282-
DataSource fileSource = tryResolveImageFileDataSourceFromDisk(baseDir, srcLocation);
283-
return (fileSource != null) ? fileSource : tryResolveFileDataSourceFromClassPath(baseClassPath, srcLocation);
303+
DataSource dataSource;
304+
305+
if (baseClassPath == null) {
306+
dataSource = tryLoadingFromClassPath(srcLocation);
307+
} else {
308+
if (srcLocation.startsWith(baseClassPath)) {
309+
dataSource = tryLoadingFromClassPath(srcLocation);
310+
} else {
311+
dataSource = tryLoadingFromClassPath(baseClassPath + srcLocation);
312+
if (dataSource == null && allowOutsideBaseClassPath) {
313+
dataSource = tryLoadingFromClassPath(srcLocation);
314+
}
315+
}
316+
}
317+
return dataSource;
284318
}
285319

286320
@Nullable
287-
private static DataSource tryResolveImageFileDataSourceFromDisk(final @Nullable String baseDir, final @NotNull String srcLocation) {
288-
File file = new File(srcLocation);
289-
if (!file.exists() && !file.isAbsolute()) {
290-
file = new File(ofNullable(baseDir).orElse("."), srcLocation);
321+
public static DataSource tryResolveUrlDataSource(@Nullable final URL baseUrl, final boolean allowOutsideBaseUrl, @NotNull final String srcLocation)
322+
throws IOException {
323+
DataSource dataSource;
324+
325+
if (baseUrl == null) {
326+
dataSource = tryLoadingFromUrl(srcLocation);
327+
} else {
328+
if (isCorrectlyFormattedUrl(srcLocation) && new URL(srcLocation).getPath().startsWith(baseUrl.getPath())) {
329+
dataSource = tryLoadingFromUrl(srcLocation);
330+
} else {
331+
final String urlPath = (baseUrl.getAuthority() + baseUrl.getPath() + "/" + srcLocation)
332+
.replaceAll("/\\\\", "/")
333+
.replaceAll("//", "/");
334+
final String url = format("%s://%s", baseUrl.getProtocol(), urlPath);
335+
336+
dataSource = tryLoadingFromUrl(url);
337+
if (dataSource == null && allowOutsideBaseUrl) {
338+
dataSource = tryLoadingFromUrl(srcLocation);
339+
}
340+
}
291341
}
292-
if (file.exists()) {
293-
final FileDataSource fileDataSource = new FileDataSource(file);
342+
return dataSource;
343+
}
344+
345+
@Nullable
346+
private static DataSource tryLoadingFromDisk(@NotNull final File srcLocation) {
347+
if (srcLocation.exists()) {
348+
final FileDataSource fileDataSource = new FileDataSource(srcLocation);
294349
fileDataSource.setFileTypeMap(ImageMimeType.IMAGE_MIMETYPES_FILE_TYPE_MAP);
295350
return fileDataSource;
296351
}
297352
return null;
298353
}
299354

300355
@Nullable
301-
private static DataSource tryResolveFileDataSourceFromClassPath(final @Nullable String baseClassPath, final @NotNull String srcLocation)
356+
private static DataSource tryLoadingFromClassPath(final @NotNull String resourceName)
302357
throws IOException {
303-
final String resourceName = (ofNullable(baseClassPath).orElse("") + srcLocation).replaceAll("//", "/");
304-
final InputStream is = MiscUtil.class.getResourceAsStream(resourceName);
358+
final String cleanResourceName = resourceName.replaceAll("//", "/");
359+
final InputStream is = MiscUtil.class.getResourceAsStream(cleanResourceName);
305360

306361
if (is != null) {
307362
try {
308-
final String mimeType = ImageMimeType.getContentType(srcLocation);
363+
final String mimeType = ImageMimeType.getContentType(resourceName);
309364
final ByteArrayDataSource ds = new ByteArrayDataSource(is, mimeType);
310365
// EMAIL-125: set the name of the DataSource to the normalized resource URL similar to other DataSource implementations, e.g. FileDataSource, URLDataSource
311-
ds.setName(MiscUtil.class.getResource(resourceName).toString());
366+
ds.setName(MiscUtil.class.getResource(cleanResourceName).toString());
312367
return ds;
313368
} finally {
314369
is.close();
@@ -317,16 +372,24 @@ private static DataSource tryResolveFileDataSourceFromClassPath(final @Nullable
317372
return null;
318373
}
319374

320-
@NotNull
321-
public static DataSource resolveUrlDataSource(@Nullable final URL baseUrl, @NotNull final String srcLocation)
322-
throws IOException {
323-
final URL url = (valueNullOrEmpty(baseUrl) || ABSOLUTE_URL_PATTERN.matcher(srcLocation).matches())
324-
? new URL(srcLocation)
325-
: new URL(baseUrl, srcLocation.replaceAll("&amp;", "&"));
375+
@Nullable
376+
private static DataSource tryLoadingFromUrl(final String url) {
377+
try {
378+
final DataSource result = new URLDataSource(new URL(url));
379+
result.getInputStream();
380+
return result;
381+
} catch (IOException e) {
382+
return null;
383+
}
384+
}
326385

327-
DataSource result = new URLDataSource(url);
328-
result.getInputStream();
329-
return result;
386+
public static boolean isCorrectlyFormattedUrl(final String srcLocation) {
387+
try {
388+
new URL(srcLocation);
389+
return true;
390+
} catch (IOException e) {
391+
return false;
392+
}
330393
}
331394

332395
public static String randomCid10() {

0 commit comments

Comments
 (0)