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.io.UnsupportedEncodingException; 023import java.net.MalformedURLException; 024import java.net.URL; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Locale; 028import java.util.Map; 029 030import javax.activation.DataHandler; 031import javax.activation.DataSource; 032import javax.activation.FileDataSource; 033import javax.activation.URLDataSource; 034import javax.mail.BodyPart; 035import javax.mail.MessagingException; 036import javax.mail.internet.MimeBodyPart; 037import javax.mail.internet.MimeMultipart; 038 039/** 040 * An HTML multipart email. 041 * 042 * <p>This class is used to send HTML formatted email. A text message 043 * can also be set for HTML unaware email clients, such as text-based 044 * email clients. 045 * 046 * <p>This class also inherits from {@link MultiPartEmail}, so it is easy to 047 * add attachments to the email. 048 * 049 * <p>To send an email in HTML, one should create a <code>HtmlEmail</code>, then 050 * use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods. 051 * The HTML content can be set with the {@link #setHtmlMsg(String)} method. The 052 * alternative text content can be set with {@link #setTextMsg(String)}. 053 * 054 * <p>Either the text or HTML can be omitted, in which case the "main" 055 * part of the multipart becomes whichever is supplied rather than a 056 * <code>multipart/alternative</code>. 057 * 058 * <h3>Embedding Images and Media</h3> 059 * 060 * <p>It is also possible to embed URLs, files, or arbitrary 061 * <code>DataSource</code>s directly into the body of the mail: 062 * <pre> 063 * HtmlEmail he = new HtmlEmail(); 064 * File img = new File("my/image.gif"); 065 * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class 066 * StringBuffer msg = new StringBuffer(); 067 * msg.append("<html><body>"); 068 * msg.append("<img src=cid:").append(he.embed(img)).append(">"); 069 * msg.append("<img src=cid:").append(he.embed(png)).append(">"); 070 * msg.append("</body></html>"); 071 * he.setHtmlMsg(msg.toString()); 072 * // code to set the other email fields (not shown) 073 * </pre> 074 * 075 * <p>Embedded entities are tracked by their name, which for <code>File</code>s is 076 * the filename itself and for <code>URL</code>s is the canonical path. It is 077 * an error to bind the same name to more than one entity, and this class will 078 * attempt to validate that for <code>File</code>s and <code>URL</code>s. When 079 * embedding a <code>DataSource</code>, the code uses the <code>equals()</code> 080 * method defined on the <code>DataSource</code>s to make the determination. 081 * 082 * @since 1.0 083 * @version $Id: HtmlEmail.html 952467 2015-05-23 18:45:36Z tn $ 084 */ 085public class HtmlEmail extends MultiPartEmail 086{ 087 /** Definition of the length of generated CID's. */ 088 public static final int CID_LENGTH = 10; 089 090 /** prefix for default HTML mail. */ 091 private static final String HTML_MESSAGE_START = "<html><body><pre>"; 092 /** suffix for default HTML mail. */ 093 private static final String HTML_MESSAGE_END = "</pre></body></html>"; 094 095 096 /** 097 * Text part of the message. This will be used as alternative text if 098 * the email client does not support HTML messages. 099 */ 100 protected String text; 101 102 /** Html part of the message. */ 103 protected String html; 104 105 /** 106 * @deprecated As of commons-email 1.1, no longer used. Inline embedded 107 * objects are now stored in {@link #inlineEmbeds}. 108 */ 109 @Deprecated 110 protected List<InlineImage> inlineImages; 111 112 /** 113 * Embedded images Map<String, InlineImage> where the key is the 114 * user-defined image name. 115 */ 116 protected Map<String, InlineImage> inlineEmbeds = new HashMap<String, InlineImage>(); 117 118 /** 119 * Set the text content. 120 * 121 * @param aText A String. 122 * @return An HtmlEmail. 123 * @throws EmailException see javax.mail.internet.MimeBodyPart 124 * for definitions 125 * @since 1.0 126 */ 127 public HtmlEmail setTextMsg(final String aText) throws EmailException 128 { 129 if (EmailUtils.isEmpty(aText)) 130 { 131 throw new EmailException("Invalid message supplied"); 132 } 133 134 this.text = aText; 135 return this; 136 } 137 138 /** 139 * Set the HTML content. 140 * 141 * @param aHtml A String. 142 * @return An HtmlEmail. 143 * @throws EmailException see javax.mail.internet.MimeBodyPart 144 * for definitions 145 * @since 1.0 146 */ 147 public HtmlEmail setHtmlMsg(final String aHtml) throws EmailException 148 { 149 if (EmailUtils.isEmpty(aHtml)) 150 { 151 throw new EmailException("Invalid message supplied"); 152 } 153 154 this.html = aHtml; 155 return this; 156 } 157 158 /** 159 * Set the message. 160 * 161 * <p>This method overrides {@link MultiPartEmail#setMsg(String)} in 162 * order to send an HTML message instead of a plain text message in 163 * the mail body. The message is formatted in HTML for the HTML 164 * part of the message; it is left as is in the alternate text 165 * part. 166 * 167 * @param msg the message text to use 168 * @return this <code>HtmlEmail</code> 169 * @throws EmailException if msg is null or empty; 170 * see javax.mail.internet.MimeBodyPart for definitions 171 * @since 1.0 172 */ 173 @Override 174 public Email setMsg(final String msg) throws EmailException 175 { 176 if (EmailUtils.isEmpty(msg)) 177 { 178 throw new EmailException("Invalid message supplied"); 179 } 180 181 setTextMsg(msg); 182 183 final StringBuffer htmlMsgBuf = new StringBuffer( 184 msg.length() 185 + HTML_MESSAGE_START.length() 186 + HTML_MESSAGE_END.length() 187 ); 188 189 htmlMsgBuf.append(HTML_MESSAGE_START) 190 .append(msg) 191 .append(HTML_MESSAGE_END); 192 193 setHtmlMsg(htmlMsgBuf.toString()); 194 195 return this; 196 } 197 198 /** 199 * Attempts to parse the specified <code>String</code> as a URL that will 200 * then be embedded in the message. 201 * 202 * @param urlString String representation of the URL. 203 * @param name The name that will be set in the filename header field. 204 * @return A String with the Content-ID of the URL. 205 * @throws EmailException when URL supplied is invalid or if {@code name} is null 206 * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions 207 * 208 * @see #embed(URL, String) 209 * @since 1.1 210 */ 211 public String embed(final String urlString, final String name) throws EmailException 212 { 213 try 214 { 215 return embed(new URL(urlString), name); 216 } 217 catch (final MalformedURLException e) 218 { 219 throw new EmailException("Invalid URL", e); 220 } 221 } 222 223 /** 224 * Embeds an URL in the HTML. 225 * 226 * <p>This method embeds a file located by an URL into 227 * the mail body. It allows, for instance, to add inline images 228 * to the email. Inline files may be referenced with a 229 * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID 230 * returned by the embed function. It is an error to bind the same name 231 * to more than one URL; if the same URL is embedded multiple times, the 232 * same Content-ID is guaranteed to be returned. 233 * 234 * <p>While functionally the same as passing <code>URLDataSource</code> to 235 * {@link #embed(DataSource, String, String)}, this method attempts 236 * to validate the URL before embedding it in the message and will throw 237 * <code>EmailException</code> if the validation fails. In this case, the 238 * <code>HtmlEmail</code> object will not be changed. 239 * 240 * <p> 241 * NOTE: Clients should take care to ensure that different URLs are bound to 242 * different names. This implementation tries to detect this and throw 243 * <code>EmailException</code>. However, it is not guaranteed to catch 244 * all cases, especially when the URL refers to a remote HTTP host that 245 * may be part of a virtual host cluster. 246 * 247 * @param url The URL of the file. 248 * @param name The name that will be set in the filename header 249 * field. 250 * @return A String with the Content-ID of the file. 251 * @throws EmailException when URL supplied is invalid or if {@code name} is null 252 * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions 253 * @since 1.0 254 */ 255 public String embed(final URL url, final String name) throws EmailException 256 { 257 if (EmailUtils.isEmpty(name)) 258 { 259 throw new EmailException("name cannot be null or empty"); 260 } 261 262 // check if a URLDataSource for this name has already been attached; 263 // if so, return the cached CID value. 264 if (inlineEmbeds.containsKey(name)) 265 { 266 final InlineImage ii = inlineEmbeds.get(name); 267 final URLDataSource urlDataSource = (URLDataSource) ii.getDataSource(); 268 // make sure the supplied URL points to the same thing 269 // as the one already associated with this name. 270 // NOTE: Comparing URLs with URL.equals() is a blocking operation 271 // in the case of a network failure therefore we use 272 // url.toExternalForm().equals() here. 273 if (url.toExternalForm().equals(urlDataSource.getURL().toExternalForm())) 274 { 275 return ii.getCid(); 276 } 277 throw new EmailException("embedded name '" + name 278 + "' is already bound to URL " + urlDataSource.getURL() 279 + "; existing names cannot be rebound"); 280 } 281 282 // verify that the URL is valid 283 InputStream is = null; 284 try 285 { 286 is = url.openStream(); 287 } 288 catch (final IOException e) 289 { 290 throw new EmailException("Invalid URL", e); 291 } 292 finally 293 { 294 try 295 { 296 if (is != null) 297 { 298 is.close(); 299 } 300 } 301 catch (final IOException ioe) // NOPMD 302 { /* sigh */ } 303 } 304 305 return embed(new URLDataSource(url), name); 306 } 307 308 /** 309 * Embeds a file in the HTML. This implementation delegates to 310 * {@link #embed(File, String)}. 311 * 312 * @param file The <code>File</code> object to embed 313 * @return A String with the Content-ID of the file. 314 * @throws EmailException when the supplied <code>File</code> cannot be 315 * used; also see {@link javax.mail.internet.MimeBodyPart} for definitions 316 * 317 * @see #embed(File, String) 318 * @since 1.1 319 */ 320 public String embed(final File file) throws EmailException 321 { 322 final String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase(Locale.ENGLISH); 323 return embed(file, cid); 324 } 325 326 /** 327 * Embeds a file in the HTML. 328 * 329 * <p>This method embeds a file located by an URL into 330 * the mail body. It allows, for instance, to add inline images 331 * to the email. Inline files may be referenced with a 332 * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID 333 * returned by the embed function. Files are bound to their names, which is 334 * the value returned by {@link java.io.File#getName()}. If the same file 335 * is embedded multiple times, the same CID is guaranteed to be returned. 336 * 337 * <p>While functionally the same as passing <code>FileDataSource</code> to 338 * {@link #embed(DataSource, String, String)}, this method attempts 339 * to validate the file before embedding it in the message and will throw 340 * <code>EmailException</code> if the validation fails. In this case, the 341 * <code>HtmlEmail</code> object will not be changed. 342 * 343 * @param file The <code>File</code> to embed 344 * @param cid the Content-ID to use for the embedded <code>File</code> 345 * @return A String with the Content-ID of the file. 346 * @throws EmailException when the supplied <code>File</code> cannot be used 347 * or if the file has already been embedded; 348 * also see {@link javax.mail.internet.MimeBodyPart} for definitions 349 * @since 1.1 350 */ 351 public String embed(final File file, final String cid) throws EmailException 352 { 353 if (EmailUtils.isEmpty(file.getName())) 354 { 355 throw new EmailException("file name cannot be null or empty"); 356 } 357 358 // verify that the File can provide a canonical path 359 String filePath = null; 360 try 361 { 362 filePath = file.getCanonicalPath(); 363 } 364 catch (final IOException ioe) 365 { 366 throw new EmailException("couldn't get canonical path for " 367 + file.getName(), ioe); 368 } 369 370 // check if a FileDataSource for this name has already been attached; 371 // if so, return the cached CID value. 372 if (inlineEmbeds.containsKey(file.getName())) 373 { 374 final InlineImage ii = inlineEmbeds.get(file.getName()); 375 final FileDataSource fileDataSource = (FileDataSource) ii.getDataSource(); 376 // make sure the supplied file has the same canonical path 377 // as the one already associated with this name. 378 String existingFilePath = null; 379 try 380 { 381 existingFilePath = fileDataSource.getFile().getCanonicalPath(); 382 } 383 catch (final IOException ioe) 384 { 385 throw new EmailException("couldn't get canonical path for file " 386 + fileDataSource.getFile().getName() 387 + "which has already been embedded", ioe); 388 } 389 if (filePath.equals(existingFilePath)) 390 { 391 return ii.getCid(); 392 } 393 throw new EmailException("embedded name '" + file.getName() 394 + "' is already bound to file " + existingFilePath 395 + "; existing names cannot be rebound"); 396 } 397 398 // verify that the file is valid 399 if (!file.exists()) 400 { 401 throw new EmailException("file " + filePath + " doesn't exist"); 402 } 403 if (!file.isFile()) 404 { 405 throw new EmailException("file " + filePath + " isn't a normal file"); 406 } 407 if (!file.canRead()) 408 { 409 throw new EmailException("file " + filePath + " isn't readable"); 410 } 411 412 return embed(new FileDataSource(file), file.getName(), cid); 413 } 414 415 /** 416 * Embeds the specified <code>DataSource</code> in the HTML using a 417 * randomly generated Content-ID. Returns the generated Content-ID string. 418 * 419 * @param dataSource the <code>DataSource</code> to embed 420 * @param name the name that will be set in the filename header field 421 * @return the generated Content-ID for this <code>DataSource</code> 422 * @throws EmailException if the embedding fails or if <code>name</code> is 423 * null or empty 424 * @see #embed(DataSource, String, String) 425 * @since 1.1 426 */ 427 public String embed(final DataSource dataSource, final String name) throws EmailException 428 { 429 // check if the DataSource has already been attached; 430 // if so, return the cached CID value. 431 if (inlineEmbeds.containsKey(name)) 432 { 433 final InlineImage ii = inlineEmbeds.get(name); 434 // make sure the supplied URL points to the same thing 435 // as the one already associated with this name. 436 if (dataSource.equals(ii.getDataSource())) 437 { 438 return ii.getCid(); 439 } 440 throw new EmailException("embedded DataSource '" + name 441 + "' is already bound to name " + ii.getDataSource().toString() 442 + "; existing names cannot be rebound"); 443 } 444 445 final String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase(); 446 return embed(dataSource, name, cid); 447 } 448 449 /** 450 * Embeds the specified <code>DataSource</code> in the HTML using the 451 * specified Content-ID. Returns the specified Content-ID string. 452 * 453 * @param dataSource the <code>DataSource</code> to embed 454 * @param name the name that will be set in the filename header field 455 * @param cid the Content-ID to use for this <code>DataSource</code> 456 * @return the URL encoded Content-ID for this <code>DataSource</code> 457 * @throws EmailException if the embedding fails or if <code>name</code> is 458 * null or empty 459 * @since 1.1 460 */ 461 public String embed(final DataSource dataSource, final String name, final String cid) 462 throws EmailException 463 { 464 if (EmailUtils.isEmpty(name)) 465 { 466 throw new EmailException("name cannot be null or empty"); 467 } 468 469 final MimeBodyPart mbp = new MimeBodyPart(); 470 471 try 472 { 473 // URL encode the cid according to RFC 2392 474 String encodedCid = EmailUtils.encodeUrl(cid); 475 476 mbp.setDataHandler(new DataHandler(dataSource)); 477 mbp.setFileName(name); 478 mbp.setDisposition(EmailAttachment.INLINE); 479 mbp.setContentID("<" + encodedCid + ">"); 480 481 final InlineImage ii = new InlineImage(encodedCid, dataSource, mbp); 482 this.inlineEmbeds.put(name, ii); 483 484 return encodedCid; 485 } 486 catch (final MessagingException me) 487 { 488 throw new EmailException(me); 489 } 490 catch (final UnsupportedEncodingException uee) 491 { 492 throw new EmailException(uee); 493 } 494 } 495 496 /** 497 * Does the work of actually building the MimeMessage. Please note that 498 * a user rarely calls this method directly and only if he/she is 499 * interested in the sending the underlying MimeMessage without 500 * commons-email. 501 * 502 * @exception EmailException if there was an error. 503 * @since 1.0 504 */ 505 @Override 506 public void buildMimeMessage() throws EmailException 507 { 508 try 509 { 510 build(); 511 } 512 catch (final MessagingException me) 513 { 514 throw new EmailException(me); 515 } 516 super.buildMimeMessage(); 517 } 518 519 /** 520 * @throws EmailException EmailException 521 * @throws MessagingException MessagingException 522 */ 523 private void build() throws MessagingException, EmailException 524 { 525 final MimeMultipart rootContainer = this.getContainer(); 526 MimeMultipart bodyEmbedsContainer = rootContainer; 527 MimeMultipart bodyContainer = rootContainer; 528 MimeBodyPart msgHtml = null; 529 MimeBodyPart msgText = null; 530 531 rootContainer.setSubType("mixed"); 532 533 // determine how to form multiparts of email 534 535 if (EmailUtils.isNotEmpty(this.html) && this.inlineEmbeds.size() > 0) 536 { 537 //If HTML body and embeds are used, create a related container and add it to the root container 538 bodyEmbedsContainer = new MimeMultipart("related"); 539 bodyContainer = bodyEmbedsContainer; 540 this.addPart(bodyEmbedsContainer, 0); 541 542 // If TEXT body was specified, create a alternative container and add it to the embeds container 543 if (EmailUtils.isNotEmpty(this.text)) 544 { 545 bodyContainer = new MimeMultipart("alternative"); 546 final BodyPart bodyPart = createBodyPart(); 547 try 548 { 549 bodyPart.setContent(bodyContainer); 550 bodyEmbedsContainer.addBodyPart(bodyPart, 0); 551 } 552 catch (final MessagingException me) 553 { 554 throw new EmailException(me); 555 } 556 } 557 } 558 else if (EmailUtils.isNotEmpty(this.text) && EmailUtils.isNotEmpty(this.html)) 559 { 560 // EMAIL-142: if we have both an HTML and TEXT body, but no attachments or 561 // inline images, the root container should have mimetype 562 // "multipart/alternative". 563 // reference: http://tools.ietf.org/html/rfc2046#section-5.1.4 564 if (this.inlineEmbeds.size() > 0 || isBoolHasAttachments()) 565 { 566 // If both HTML and TEXT bodies are provided, create an alternative 567 // container and add it to the root container 568 bodyContainer = new MimeMultipart("alternative"); 569 this.addPart(bodyContainer, 0); 570 } 571 else 572 { 573 // no attachments or embedded images present, change the mimetype 574 // of the root container (= body container) 575 rootContainer.setSubType("alternative"); 576 } 577 } 578 579 if (EmailUtils.isNotEmpty(this.html)) 580 { 581 msgHtml = new MimeBodyPart(); 582 bodyContainer.addBodyPart(msgHtml, 0); 583 584 // EMAIL-104: call explicitly setText to use default mime charset 585 // (property "mail.mime.charset") in case none has been set 586 msgHtml.setText(this.html, this.charset, EmailConstants.TEXT_SUBTYPE_HTML); 587 588 // EMAIL-147: work-around for buggy JavaMail implementations; 589 // in case setText(...) does not set the correct content type, 590 // use the setContent() method instead. 591 final String contentType = msgHtml.getContentType(); 592 if (contentType == null || !contentType.equals(EmailConstants.TEXT_HTML)) 593 { 594 // apply default charset if one has been set 595 if (EmailUtils.isNotEmpty(this.charset)) 596 { 597 msgHtml.setContent(this.html, EmailConstants.TEXT_HTML + "; charset=" + this.charset); 598 } 599 else 600 { 601 // unfortunately, MimeUtility.getDefaultMIMECharset() is package private 602 // and thus can not be used to set the default system charset in case 603 // no charset has been provided by the user 604 msgHtml.setContent(this.html, EmailConstants.TEXT_HTML); 605 } 606 } 607 608 for (final InlineImage image : this.inlineEmbeds.values()) 609 { 610 bodyEmbedsContainer.addBodyPart(image.getMbp()); 611 } 612 } 613 614 if (EmailUtils.isNotEmpty(this.text)) 615 { 616 msgText = new MimeBodyPart(); 617 bodyContainer.addBodyPart(msgText, 0); 618 619 // EMAIL-104: call explicitly setText to use default mime charset 620 // (property "mail.mime.charset") in case none has been set 621 msgText.setText(this.text, this.charset); 622 } 623 } 624 625 /** 626 * Private bean class that encapsulates data about URL contents 627 * that are embedded in the final email. 628 * @since 1.1 629 */ 630 private static class InlineImage 631 { 632 /** content id. */ 633 private final String cid; 634 /** <code>DataSource</code> for the content. */ 635 private final DataSource dataSource; 636 /** the <code>MimeBodyPart</code> that contains the encoded data. */ 637 private final MimeBodyPart mbp; 638 639 /** 640 * Creates an InlineImage object to represent the 641 * specified content ID and <code>MimeBodyPart</code>. 642 * @param cid the generated content ID 643 * @param dataSource the <code>DataSource</code> that represents the content 644 * @param mbp the <code>MimeBodyPart</code> that contains the encoded 645 * data 646 */ 647 public InlineImage(final String cid, final DataSource dataSource, final MimeBodyPart mbp) 648 { 649 this.cid = cid; 650 this.dataSource = dataSource; 651 this.mbp = mbp; 652 } 653 654 /** 655 * Returns the unique content ID of this InlineImage. 656 * @return the unique content ID of this InlineImage 657 */ 658 public String getCid() 659 { 660 return cid; 661 } 662 663 /** 664 * Returns the <code>DataSource</code> that represents the encoded content. 665 * @return the <code>DataSource</code> representing the encoded content 666 */ 667 public DataSource getDataSource() 668 { 669 return dataSource; 670 } 671 672 /** 673 * Returns the <code>MimeBodyPart</code> that contains the 674 * encoded InlineImage data. 675 * @return the <code>MimeBodyPart</code> containing the encoded 676 * InlineImage data 677 */ 678 public MimeBodyPart getMbp() 679 { 680 return mbp; 681 } 682 683 // equals()/hashCode() implementations, since this class 684 // is stored as a entry in a Map. 685 /** 686 * {@inheritDoc} 687 * @return true if the other object is also an InlineImage with the same cid. 688 */ 689 @Override 690 public boolean equals(final Object obj) 691 { 692 if (this == obj) 693 { 694 return true; 695 } 696 if (!(obj instanceof InlineImage)) 697 { 698 return false; 699 } 700 701 final InlineImage that = (InlineImage) obj; 702 703 return this.cid.equals(that.cid); 704 } 705 706 /** 707 * {@inheritDoc} 708 * @return the cid hashCode. 709 */ 710 @Override 711 public int hashCode() 712 { 713 return cid.hashCode(); 714 } 715 } 716}