001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.mail; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.net.MalformedURLException; 023import java.net.URL; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Objects; 028 029import javax.activation.DataHandler; 030import javax.activation.DataSource; 031import javax.activation.FileDataSource; 032import javax.activation.URLDataSource; 033import javax.mail.BodyPart; 034import javax.mail.MessagingException; 035import javax.mail.internet.MimeBodyPart; 036import javax.mail.internet.MimeMultipart; 037 038/** 039 * An HTML multipart email. 040 * <p> 041 * This class is used to send HTML formatted email. A text message can also be set for HTML unaware email clients, such as text-based email clients. 042 * </p> 043 * <p> 044 * This class also inherits from {@link MultiPartEmail}, so it is easy to add attachments to the email. 045 * </p> 046 * <p> 047 * To send an email in HTML, one should create a {@code HtmlEmail}, then use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods. The HTML content 048 * can be set with the {@link #setHtmlMsg(String)} method. The alternative text content can be set with {@link #setTextMsg(String)}. 049 * </p> 050 * <p> 051 * Either the text or HTML can be omitted, in which case the "main" part of the multipart becomes whichever is supplied rather than a 052 * {@code multipart/alternative}. 053 * </p> 054 * <h2>Embedding Images and Media</h2> 055 * <p> 056 * It is also possible to embed URLs, files, or arbitrary {@code DataSource}s directly into the body of the mail: 057 * </p> 058 * 059 * <pre> 060 * HtmlEmail he = new HtmlEmail(); 061 * File img = new File("my/image.gif"); 062 * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class 063 * StringBuffer msg = new StringBuffer(); 064 * msg.append("<html><body>"); 065 * msg.append("<img src=cid:").append(he.embed(img)).append(">"); 066 * msg.append("<img src=cid:").append(he.embed(png)).append(">"); 067 * msg.append("</body></html>"); 068 * he.setHtmlMsg(msg.toString()); 069 * // code to set the other email fields (not shown) 070 * </pre> 071 * <p> 072 * Embedded entities are tracked by their name, which for {@code File}s is the file name itself and for {@code URL}s is the canonical path. It is an error to 073 * bind the same name to more than one entity, and this class will attempt to validate that for {@code File}s and {@code URL}s. When embedding a 074 * {@code DataSource}, the code uses the {@code equals()} method defined on the {@code DataSource}s to make the determination. 075 * </p> 076 * 077 * @since 1.0 078 */ 079public class HtmlEmail extends MultiPartEmail { 080 081 /** 082 * Private bean class that encapsulates data about URL contents that are embedded in the final email. 083 * 084 * @since 1.1 085 */ 086 private static final class InlineImage { 087 088 /** Content id. */ 089 private final String cid; 090 091 /** {@code DataSource} for the content. */ 092 private final DataSource dataSource; 093 094 /** The {@code MimeBodyPart} that contains the encoded data. */ 095 private final MimeBodyPart mimeBodyPart; 096 097 /** 098 * Creates an InlineImage object to represent the specified content ID and {@code MimeBodyPart}. 099 * 100 * @param cid the generated content ID, not null. 101 * @param dataSource the {@code DataSource} that represents the content, not null. 102 * @param mimeBodyPart the {@code MimeBodyPart} that contains the encoded data, not null. 103 */ 104 private InlineImage(final String cid, final DataSource dataSource, final MimeBodyPart mimeBodyPart) { 105 this.cid = Objects.requireNonNull(cid, "cid"); 106 this.dataSource = Objects.requireNonNull(dataSource, "dataSource"); 107 this.mimeBodyPart = Objects.requireNonNull(mimeBodyPart, "mimeBodyPart"); 108 } 109 110 @Override 111 public boolean equals(final Object obj) { 112 if (this == obj) { 113 return true; 114 } 115 if (!(obj instanceof InlineImage)) { 116 return false; 117 } 118 final InlineImage other = (InlineImage) obj; 119 return Objects.equals(cid, other.cid); 120 } 121 122 /** 123 * Returns the unique content ID of this InlineImage. 124 * 125 * @return the unique content ID of this InlineImage 126 */ 127 private String getCid() { 128 return cid; 129 } 130 131 /** 132 * Returns the {@code DataSource} that represents the encoded content. 133 * 134 * @return the {@code DataSource} representing the encoded content 135 */ 136 private DataSource getDataSource() { 137 return dataSource; 138 } 139 140 /** 141 * Returns the {@code MimeBodyPart} that contains the encoded InlineImage data. 142 * 143 * @return the {@code MimeBodyPart} containing the encoded InlineImage data 144 */ 145 private MimeBodyPart getMimeBodyPart() { 146 return mimeBodyPart; 147 } 148 149 @Override 150 public int hashCode() { 151 return Objects.hash(cid); 152 } 153 } 154 155 /** Definition of the length of generated CID's. */ 156 public static final int CID_LENGTH = 10; 157 158 /** Prefix for default HTML mail. */ 159 private static final String HTML_MESSAGE_START = "<html><body><pre>"; 160 161 /** Suffix for default HTML mail. */ 162 private static final String HTML_MESSAGE_END = "</pre></body></html>"; 163 164 /** 165 * Text part of the message. This will be used as alternative text if the email client does not support HTML messages. 166 * 167 * @deprecated Use getters and getters. 168 */ 169 @Deprecated 170 protected String text; 171 172 /** 173 * HTML part of the message. 174 * 175 * @deprecated Use getters and getters. 176 */ 177 @Deprecated 178 protected String html; 179 180 /** 181 * @deprecated As of commons-email 1.1, no longer used. Inline embedded objects are now stored in {@link #inlineEmbeds}. 182 */ 183 @Deprecated 184 protected List<InlineImage> inlineImages; 185 186 /** 187 * Embedded images Map<String, InlineImage> where the key is the user-defined image name. 188 * 189 * @deprecated Use getters and getters. 190 */ 191 @Deprecated 192 protected Map<String, InlineImage> inlineEmbeds = new HashMap<>(); 193 194 /** 195 * Constructs a new instance. 196 */ 197 public HtmlEmail() { 198 // empty 199 } 200 201 /** 202 * @throws EmailException EmailException 203 * @throws MessagingException MessagingException 204 */ 205 private void build() throws MessagingException, EmailException { 206 final MimeMultipart rootContainer = getContainer(); 207 MimeMultipart bodyEmbedsContainer = rootContainer; 208 MimeMultipart bodyContainer = rootContainer; 209 MimeBodyPart msgHtml = null; 210 MimeBodyPart msgText = null; 211 212 rootContainer.setSubType("mixed"); 213 214 // determine how to form multiparts of email 215 216 if (EmailUtils.isNotEmpty(html) && !EmailUtils.isEmpty(inlineEmbeds)) { 217 // If HTML body and embeds are used, create a related container and add it to the root container 218 bodyEmbedsContainer = new MimeMultipart("related"); 219 bodyContainer = bodyEmbedsContainer; 220 addPart(bodyEmbedsContainer, 0); 221 222 // If TEXT body was specified, create a alternative container and add it to the embeds container 223 if (EmailUtils.isNotEmpty(text)) { 224 bodyContainer = new MimeMultipart("alternative"); 225 final BodyPart bodyPart = createBodyPart(); 226 try { 227 bodyPart.setContent(bodyContainer); 228 bodyEmbedsContainer.addBodyPart(bodyPart, 0); 229 } catch (final MessagingException e) { 230 throw new EmailException(e); 231 } 232 } 233 } else if (EmailUtils.isNotEmpty(text) && EmailUtils.isNotEmpty(html)) { 234 // EMAIL-142: if we have both an HTML and TEXT body, but no attachments or 235 // inline images, the root container should have mimetype 236 // "multipart/alternative". 237 // reference: http://tools.ietf.org/html/rfc2046#section-5.1.4 238 if (!EmailUtils.isEmpty(inlineEmbeds) || isBoolHasAttachments()) { 239 // If both HTML and TEXT bodies are provided, create an alternative 240 // container and add it to the root container 241 bodyContainer = new MimeMultipart("alternative"); 242 this.addPart(bodyContainer, 0); 243 } else { 244 // no attachments or embedded images present, change the mimetype 245 // of the root container (= body container) 246 rootContainer.setSubType("alternative"); 247 } 248 } 249 250 if (EmailUtils.isNotEmpty(html)) { 251 msgHtml = new MimeBodyPart(); 252 bodyContainer.addBodyPart(msgHtml, 0); 253 254 // EMAIL-104: call explicitly setText to use default mime charset 255 // (property "mail.mime.charset") in case none has been set 256 msgHtml.setText(html, getCharsetName(), EmailConstants.TEXT_SUBTYPE_HTML); 257 258 // EMAIL-147: work-around for buggy JavaMail implementations; 259 // in case setText(...) does not set the correct content type, 260 // use the setContent() method instead. 261 final String contentType = msgHtml.getContentType(); 262 if (contentType == null || !contentType.equals(EmailConstants.TEXT_HTML)) { 263 // apply default charset if one has been set 264 if (EmailUtils.isNotEmpty(getCharsetName())) { 265 msgHtml.setContent(html, EmailConstants.TEXT_HTML + "; charset=" + getCharsetName()); 266 } else { 267 // unfortunately, MimeUtility.getDefaultMIMECharset() is package private 268 // and thus can not be used to set the default system charset in case 269 // no charset has been provided by the user 270 msgHtml.setContent(html, EmailConstants.TEXT_HTML); 271 } 272 } 273 274 for (final InlineImage image : inlineEmbeds.values()) { 275 bodyEmbedsContainer.addBodyPart(image.getMimeBodyPart()); 276 } 277 } 278 279 if (EmailUtils.isNotEmpty(text)) { 280 msgText = new MimeBodyPart(); 281 bodyContainer.addBodyPart(msgText, 0); 282 283 // EMAIL-104: call explicitly setText to use default mime charset 284 // (property "mail.mime.charset") in case none has been set 285 msgText.setText(text, getCharsetName()); 286 } 287 } 288 289 /** 290 * Builds the MimeMessage. Please note that a user rarely calls this method directly and only if he/she is interested in the sending the underlying 291 * MimeMessage without commons-email. 292 * 293 * @throws EmailException if there was an error. 294 * @since 1.0 295 */ 296 @Override 297 public void buildMimeMessage() throws EmailException { 298 try { 299 build(); 300 } catch (final MessagingException e) { 301 throw new EmailException(e); 302 } 303 super.buildMimeMessage(); 304 } 305 306 /** 307 * Embeds the specified {@code DataSource} in the HTML using a randomly generated Content-ID. Returns the generated Content-ID string. 308 * 309 * @param dataSource the {@code DataSource} to embed 310 * @param name the name that will be set in the file name header field 311 * @return the generated Content-ID for this {@code DataSource} 312 * @throws EmailException if the embedding fails or if {@code name} is null or empty 313 * @see #embed(DataSource, String, String) 314 * @since 1.1 315 */ 316 public String embed(final DataSource dataSource, final String name) throws EmailException { 317 // check if the DataSource has already been attached; 318 // if so, return the cached CID value. 319 final InlineImage inlineImage = inlineEmbeds.get(name); 320 if (inlineImage != null) { 321 // make sure the supplied URL points to the same thing 322 // as the one already associated with this name. 323 if (dataSource.equals(inlineImage.getDataSource())) { 324 return inlineImage.getCid(); 325 } 326 throw new EmailException("embedded DataSource '" + name + "' is already bound to name " + inlineImage.getDataSource().toString() 327 + "; existing names cannot be rebound"); 328 } 329 330 final String cid = EmailUtils.toLower(EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH)); 331 return embed(dataSource, name, cid); 332 } 333 334 /** 335 * Embeds the specified {@code DataSource} in the HTML using the specified Content-ID. Returns the specified Content-ID string. 336 * 337 * @param dataSource the {@code DataSource} to embed 338 * @param name the name that will be set in the file name header field 339 * @param cid the Content-ID to use for this {@code DataSource} 340 * @return the URL encoded Content-ID for this {@code DataSource} 341 * @throws EmailException if the embedding fails or if {@code name} is null or empty 342 * @since 1.1 343 */ 344 public String embed(final DataSource dataSource, final String name, final String cid) throws EmailException { 345 EmailException.checkNonEmpty(name, () -> "Name cannot be null or empty"); 346 final MimeBodyPart mbp = new MimeBodyPart(); 347 try { 348 // URL encode the cid according to RFC 2392 349 final String encodedCid = EmailUtils.encodeUrl(cid); 350 mbp.setDataHandler(new DataHandler(dataSource)); 351 mbp.setFileName(name); 352 mbp.setDisposition(EmailAttachment.INLINE); 353 mbp.setContentID("<" + encodedCid + ">"); 354 this.inlineEmbeds.put(name, new InlineImage(encodedCid, dataSource, mbp)); 355 return encodedCid; 356 } catch (final MessagingException e) { 357 throw new EmailException(e); 358 } 359 } 360 361 /** 362 * Embeds a file in the HTML. This implementation delegates to {@link #embed(File, String)}. 363 * 364 * @param file The {@code File} object to embed 365 * @return A String with the Content-ID of the file. 366 * @throws EmailException when the supplied {@code File} cannot be used; also see {@link javax.mail.internet.MimeBodyPart} for definitions 367 * 368 * @see #embed(File, String) 369 * @since 1.1 370 */ 371 public String embed(final File file) throws EmailException { 372 return embed(file, EmailUtils.toLower(EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH))); 373 } 374 375 /** 376 * Embeds a file in the HTML. 377 * 378 * <p> 379 * This method embeds a file located by an URL into the mail body. It allows, for instance, to add inline images to the email. Inline files may be 380 * referenced with a {@code cid:xxxxxx} URL, where xxxxxx is the Content-ID returned by the embed function. Files are bound to their names, which is the 381 * value returned by {@link java.io.File#getName()}. If the same file is embedded multiple times, the same CID is guaranteed to be returned. 382 * 383 * <p> 384 * While functionally the same as passing {@code FileDataSource} to {@link #embed(DataSource, String, String)}, this method attempts to validate the file 385 * before embedding it in the message and will throw {@code EmailException} if the validation fails. In this case, the {@code HtmlEmail} object will not be 386 * changed. 387 * 388 * @param file The {@code File} to embed 389 * @param cid the Content-ID to use for the embedded {@code File} 390 * @return A String with the Content-ID of the file. 391 * @throws EmailException when the supplied {@code File} cannot be used or if the file has already been embedded; also see 392 * {@link javax.mail.internet.MimeBodyPart} for definitions 393 * @since 1.1 394 */ 395 public String embed(final File file, final String cid) throws EmailException { 396 EmailException.checkNonEmpty(file.getName(), () -> "File name cannot be null or empty"); 397 398 // verify that the File can provide a canonical path 399 String filePath = null; 400 try { 401 filePath = file.getCanonicalPath(); 402 } catch (final IOException e) { 403 throw new EmailException("couldn't get canonical path for " + file.getName(), e); 404 } 405 406 // check if a FileDataSource for this name has already been attached; 407 // if so, return the cached CID value. 408 final InlineImage inlineImage = inlineEmbeds.get(file.getName()); 409 if (inlineImage != null) { 410 final FileDataSource fileDataSource = (FileDataSource) inlineImage.getDataSource(); 411 // make sure the supplied file has the same canonical path 412 // as the one already associated with this name. 413 String existingFilePath = null; 414 try { 415 existingFilePath = fileDataSource.getFile().getCanonicalPath(); 416 } catch (final IOException e) { 417 throw new EmailException("couldn't get canonical path for file " + fileDataSource.getFile().getName() + "which has already been embedded", e); 418 } 419 if (filePath.equals(existingFilePath)) { 420 return inlineImage.getCid(); 421 } 422 throw new EmailException( 423 "embedded name '" + file.getName() + "' is already bound to file " + existingFilePath + "; existing names cannot be rebound"); 424 } 425 426 // verify that the file is valid 427 if (!file.exists()) { 428 throw new EmailException("file " + filePath + " doesn't exist"); 429 } 430 if (!file.isFile()) { 431 throw new EmailException("file " + filePath + " isn't a normal file"); 432 } 433 if (!file.canRead()) { 434 throw new EmailException("file " + filePath + " isn't readable"); 435 } 436 437 return embed(new FileDataSource(file), file.getName(), cid); 438 } 439 440 /** 441 * Parses the specified {@code String} as a URL that will then be embedded in the message. 442 * 443 * @param urlString String representation of the URL. 444 * @param name The name that will be set in the file name header field. 445 * @return A String with the Content-ID of the URL. 446 * @throws EmailException when URL supplied is invalid or if {@code name} is null or empty; also see {@link javax.mail.internet.MimeBodyPart} for 447 * definitions 448 * 449 * @see #embed(URL, String) 450 * @since 1.1 451 */ 452 public String embed(final String urlString, final String name) throws EmailException { 453 try { 454 return embed(new URL(urlString), name); 455 } catch (final MalformedURLException e) { 456 throw new EmailException("Invalid URL", e); 457 } 458 } 459 460 /** 461 * Embeds an URL in the HTML. 462 * 463 * <p> 464 * This method embeds a file located by an URL into the mail body. It allows, for instance, to add inline images to the email. Inline files may be 465 * referenced with a {@code cid:xxxxxx} URL, where xxxxxx is the Content-ID returned by the embed function. It is an error to bind the same name to more 466 * than one URL; if the same URL is embedded multiple times, the same Content-ID is guaranteed to be returned. 467 * </p> 468 * <p> 469 * While functionally the same as passing {@code URLDataSource} to {@link #embed(DataSource, String, String)}, this method attempts to validate the URL 470 * before embedding it in the message and will throw {@code EmailException} if the validation fails. In this case, the {@code HtmlEmail} object will not be 471 * changed. 472 * </p> 473 * <p> 474 * NOTE: Clients should take care to ensure that different URLs are bound to different names. This implementation tries to detect this and throw 475 * {@code EmailException}. However, it is not guaranteed to catch all cases, especially when the URL refers to a remote HTTP host that may be part of a 476 * virtual host cluster. 477 * </p> 478 * 479 * @param url The URL of the file. 480 * @param name The name that will be set in the file name header field. 481 * @return A String with the Content-ID of the file. 482 * @throws EmailException when URL supplied is invalid or if {@code name} is null or empty; also see {@link javax.mail.internet.MimeBodyPart} for 483 * definitions 484 * @since 1.0 485 */ 486 public String embed(final URL url, final String name) throws EmailException { 487 EmailException.checkNonEmpty(name, () -> "Name cannot be null or empty"); 488 // check if a URLDataSource for this name has already been attached; 489 // if so, return the cached CID value. 490 final InlineImage inlineImage = inlineEmbeds.get(name); 491 if (inlineImage != null) { 492 final URLDataSource urlDataSource = (URLDataSource) inlineImage.getDataSource(); 493 // make sure the supplied URL points to the same thing 494 // as the one already associated with this name. 495 // NOTE: Comparing URLs with URL.equals() is a blocking operation 496 // in the case of a network failure therefore we use 497 // url.toExternalForm().equals() here. 498 if (url.toExternalForm().equals(urlDataSource.getURL().toExternalForm())) { 499 return inlineImage.getCid(); 500 } 501 throw new EmailException("embedded name '" + name + "' is already bound to URL " + urlDataSource.getURL() + "; existing names cannot be rebound"); 502 } 503 // verify that the URL is valid 504 try (InputStream inputStream = url.openStream()) { 505 // Make sure we can read. 506 inputStream.read(); 507 } catch (final IOException e) { 508 throw new EmailException("Invalid URL", e); 509 } 510 return embed(new URLDataSource(url), name); 511 } 512 513 /** 514 * Gets the HTML content. 515 * 516 * @return the HTML content. 517 * @since 1.6.0 518 */ 519 public String getHtml() { 520 return html; 521 } 522 523 /** 524 * Gets the message text. 525 * 526 * @return the message text. 527 * @since 1.6.0 528 */ 529 public String getText() { 530 return text; 531 } 532 533 /** 534 * Sets the HTML content. 535 * 536 * @param html A String. 537 * @return An HtmlEmail. 538 * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions 539 * @since 1.0 540 */ 541 public HtmlEmail setHtmlMsg(final String html) throws EmailException { 542 this.html = EmailException.checkNonEmpty(html, () -> "Invalid message."); 543 return this; 544 } 545 546 /** 547 * Sets the message. 548 * 549 * <p> 550 * This method overrides {@link MultiPartEmail#setMsg(String)} in order to send an HTML message instead of a plain text message in the mail body. The 551 * message is formatted in HTML for the HTML part of the message; it is left as is in the alternate text part. 552 * </p> 553 * 554 * @param msg the message text to use 555 * @return this {@code HtmlEmail} 556 * @throws EmailException if msg is null or empty; see javax.mail.internet.MimeBodyPart for definitions 557 * @since 1.0 558 */ 559 @Override 560 public Email setMsg(final String msg) throws EmailException { 561 setTextMsg(msg); 562 final StringBuilder htmlMsgBuf = new StringBuilder(msg.length() + HTML_MESSAGE_START.length() + HTML_MESSAGE_END.length()); 563 htmlMsgBuf.append(HTML_MESSAGE_START).append(msg).append(HTML_MESSAGE_END); 564 setHtmlMsg(htmlMsgBuf.toString()); 565 return this; 566 } 567 568 /** 569 * Sets the text content. 570 * 571 * @param text A String. 572 * @return An HtmlEmail. 573 * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions 574 * @since 1.0 575 */ 576 public HtmlEmail setTextMsg(final String text) throws EmailException { 577 this.text = EmailException.checkNonEmpty(text, () -> "Invalid message."); 578 return this; 579 } 580}