5

In my Spring Boot app, I'm generating HTML emails with Thymeleaf. I want to include an <img> in these emails. The image is stored at /src/main/resources/static/img/logo.png.

I've confirmed that the image can be resolved by starting the app locally and requesting http://localhost:8080/img/logo.svg in a browser.

To include this image in the HTML, I've tried all of the following

  1. <img th:src="@{/img/logo.png}" />
  2. <img th:src="@{img/logo.png}" />
  3. <img th:src="@{~/img/logo.png}" />
  4. Base64 encoded image <img src="data:image/png;base64,iVBORw0KGgoAA..." />

The outcome of each of these is:

  1. Throws an exception: org.thymeleaf.exceptions.TemplateProcessingException: Link base "/img/logo.svg" cannot be context relative (/...) unless the context used for executing the engine implements the org.thymeleaf.context.IWebContext interface
  2. Renders <img src="img/logo.png" /> which appears in the email as a broken image
  3. Renders <img src="/img/logo.png" /> which appears in the email as a broken image
  4. The image is rendered in most email clients I tested, but it's blocked by GMail, and there's no way to unblock it via the settings.

I guess that in order for the image to be rendered correctly within an email I need to provide an absolute URL, but I'm not sure how to achieve that.

Part of the problem is that it's not obvious whether an email is not being displayed because the URL is incorrect, or because the email client is blocking images.

Update

I thought this would be obvious, but evidently not: I can't use any solution which hard-codes the hostname to localhost:8080 because this is just the URL I use when running locally, and I also need this to work in other environments, e.g. prod

Dónal
  • 185,044
  • 174
  • 569
  • 824
  • the problem is "form where you view" your html.... ;) – xerx593 Jun 10 '21 at 10:34
  • 1. (my fav): [embedding-image-in-html-email](https://stackoverflow.com/q/6706891/592355) ..when you want to serve the images form a (public) server: https://stackoverflow.com/a/55658415/592355, https://stackoverflow.com/q/46981145/592355, https://www.thymeleaf.org/doc/articles/standardurlsyntax.html (hard code!) – xerx593 Jun 10 '21 at 10:38
  • 1
    @xerx593 if you have an answer, could you post it as an answer, please? – Dónal Jun 10 '21 at 10:53
  • The URL in the E-Mail has to be reachable on the target client (that means, on the device that opens the e-mail). Also, many mail webclients proxy external URLs through their own server. They may forbid hosts like "localhost". Also note that many mail clients don't support SVG Images: https://stackoverflow.com/questions/37753911/how-can-i-embed-svg-into-html-in-an-email-so-that-its-visible-in-most-all-emai – Felix Jun 10 '21 at 10:54
  • @Felix as stated in my question, I've already verified that the image can be downloaded via `http://localhost:8080/img/logo.svg`. The same problem occurs when the server run on a local or non-local host – Dónal Jun 10 '21 at 10:59
  • @Dónal I just edited my comment. Many E-Mail Clients don't support SVG Images. HTML-Mails support much less than a regular webpage does. – Felix Jun 10 '21 at 11:00
  • @Felix I tried with PNGs, same result – Dónal Jun 10 '21 at 13:57
  • Ensure that the URL in the generated HTML is absolute – Felix Jun 10 '21 at 14:03
  • @Felix my question is literally _how_ to do that – Dónal Jun 10 '21 at 14:11
  • @Dónal Sorry, I misread that then. The problem is, the server by itself only knows which local network interface and port its listening on. It doesnt know its public IP or hostname, and in more complex setups you could have loadbalancers, port-remapping and some other routing stuff that even more hides how the server is reachable for end-users. You have to hardcode the src (like in the first example of xerx593 answer). – Felix Jun 10 '21 at 15:28
  • What about setting the relative path to the image from the location of mail template like `../../resources/static/img/logo.png` – ThilankaD Jun 19 '21 at 18:41
  • regarding "gmail inline images" [see here](https://stackoverflow.com/q/41946783/592355). – xerx593 Jun 21 '21 at 20:55
  • but again: ["Using an **inline attachment with a Content-ID** works with Gmail."](https://stackoverflow.com/a/42014708/592355) – xerx593 Jun 21 '21 at 21:02

2 Answers2

2

Advanced

You introduce a property declaring the "public url" (e.g. in application.properties):

public_domain=http://somwhere.com

To use it like:

<img th:src="@{|${public_domain}/img/logo.svg|}" />

like here.


Totally Dynamic

<img th:scr="${#httpServletRequest.scheme}+'://'+${#httpServletRequest.serverName}+':'+${#httpServletRequest.serverPort}+@{img/logo.svg}" />

super cool!! (this will only work in presence of an http (servlet) request, which seems not relevant here.)


Going Deeper

You never know who "watches" your emails with whatever client(, which trusts whatever server..and loads images from it) !!? ...

So embedding image in html email is a "quite popular" question here at [so].

And applied to thymeleaf: They have an extra article for that !! (also showing img attachments .. works in html AND text(without images;()!!!;)

To summarize(, once mailing and templating are configured):

template:

 <img src="sample.png" th:src="|cid:${imageResourceName}|" />

The img element has a hardcoded src value —nice for prototyping—, which will be substituted at runtime by something like cid:image.jpg matching the attached image filename.

service:

String imageResourceName = ...
byte[] imageBytes = ...
String imageContentType = ...

// Prepare the evaluation context
final Context ctx = new Context(locale);
...
ctx.setVariable("imageResourceName", imageResourceName); // so that we can reference it from HTML

// Prepare message using a Spring helper
final MimeMessage mimeMessage = this.mailSender.createMimeMessage();
final MimeMessageHelper message = ...
message.set...

// Create the HTML body using Thymeleaf
final String htmlContent = ...

// Add the inline image, referenced from the HTML code as "cid:${imageResourceName}" !!!
final InputStreamSource imageSource = new ByteArrayResource(imageBytes);
message.addInline(imageResourceName, imageSource, imageContentType);

// Send mail ...
Dónal
  • 185,044
  • 174
  • 569
  • 824
xerx593
  • 12,237
  • 5
  • 33
  • 64
  • Note that OP doesnt use Thymeleaf in the context of a HTTPRequest. He uses Thymeleaf as a templating engine to generate HTML E-Mails – Felix Jun 10 '21 at 13:59
  • yes, "totally dynamic" will work only in that (http request) case! Still confusing: OP uses `http://localhost:8080` (probably with spring-boot ..to serve the image), but still "missing IWebContext" – xerx593 Jun 10 '21 at 14:04
  • I like your edit. For future readers, note that the example embeds the actual image data into the e-mail, so there will be no request to your webserver once the user opens the e-mail. So this way it's not possible to serve dynamic images – Felix Jun 10 '21 at 15:33
  • ..it is also not possible (by design) to send "dynamic emails"! ;) – xerx593 Jun 21 '21 at 20:56
  • (i mean: emails cannot (should not) alter after sending!?) – xerx593 Jun 21 '21 at 21:56
  • If you pass an URL into the `src` attribute of an HTML-Img in an email, the Webserver can respond with a new image every time the users open the e-mail. Thats how many personalized recommendation newsletters are implemented (the system that sends the email doesnt even know which products are being recommended to the users, the images inside the email may even change if you open it again 1 day later). That's what I meant when I said "dynamic images". You can't alter an email after sending structurally, but any ref to an external source can – Felix Jun 22 '21 at 08:08
  • yes, this case (the src-url is static!) can be handled by "Simple" and "Advanced". ..but the "best practice" "showing an image in an html email" seems to be: "Attachment with inline id" (as shown in the article;) – xerx593 Jun 22 '21 at 10:47
0

So "strategically" we have two options:

  • A: to serve the image (from a public server with a static url).
  • B: to send the image "with the email body".

(pros, cons,...)

A

We should do it as an "Absolute URL" as proposed by Thymeleaf:

Absolute URLs allow you to create links to other servers. They start by specifying a protocol name (http:// or https://) ...

They are not modified at all (unless you have an URL Rewriting filter configured at your server and performing modifications at the HttpServletResponse.encodeUrl(...) method)...

<img th:src="@{http://localhost:8080/img/logo.svg}" />

This will be rendered to:

<img src="http://localhost:8080/img/logo.svg" />

(whatever the mail client will do with it)

We can advance this approach by introducing & using some placeholder (for different environments e.g.):

(e.g.) application.properties:

public_domain=http://somewhere.com

To use it like:

<img th:src="@{|${public_domain}/img/logo.svg|}" />

like here.

B

Here we have (technically) the options to:

The outline of the Thymeleaf article, once templating and mailing are configured, is:

template html:

 <img src="/path/when/not/thymeleaf-generated/logo.svg" th:src="|cid:${imageResourceName}|" />

The img element has a hardcoded src value — nice for prototyping —, which will be substituted at runtime by something like cid:image.jpg matching the attached image filename.

service java:

String imageResourceName = ...
byte[] imageBytes = ...
String imageContentType = ... // availabe!

// Prepare the evaluation context
final Context ctx = new Context(locale);
...
ctx.setVariable("imageResourceName", imageResourceName); // so that we can reference it from HTML

// See Article...

// Add the inline image, referenced from the HTML code as "cid:${imageResourceName}" !!!
final InputStreamSource imageSource = new ByteArrayResource(imageBytes);
message.addInline(imageResourceName, imageSource, imageContentType);

// Send mail ...
xerx593
  • 12,237
  • 5
  • 33
  • 64
  • sorry, same answer, but more structre/different aspects. For "Pros & Cons", see comments, links, other answers.. :) – xerx593 Jun 22 '21 at 11:33