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.java 1606709 2014-06-30 12:26:06Z ggregory $ 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 else 278 { 279 throw new EmailException("embedded name '" + name 280 + "' is already bound to URL " + urlDataSource.getURL() 281 + "; existing names cannot be rebound"); 282 } 283 } 284 285 // verify that the URL is valid 286 InputStream is = null; 287 try 288 { 289 is = url.openStream(); 290 } 291 catch (final IOException e) 292 { 293 throw new EmailException("Invalid URL", e); 294 } 295 finally 296 { 297 try 298 { 299 if (is != null) 300 { 301 is.close(); 302 } 303 } 304 catch (final IOException ioe) // NOPMD 305 { /* sigh */ } 306 } 307 308 return embed(new URLDataSource(url), name); 309 } 310 311 /** 312 * Embeds a file in the HTML. This implementation delegates to 313 * {@link #embed(File, String)}. 314 * 315 * @param file The <code>File</code> object to embed 316 * @return A String with the Content-ID of the file. 317 * @throws EmailException when the supplied <code>File</code> cannot be 318 * used; also see {@link javax.mail.internet.MimeBodyPart} for definitions 319 * 320 * @see #embed(File, String) 321 * @since 1.1 322 */ 323 public String embed(final File file) throws EmailException 324 { 325 final String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase(Locale.ENGLISH); 326 return embed(file, cid); 327 } 328 329 /** 330 * Embeds a file in the HTML. 331 * 332 * <p>This method embeds a file located by an URL into 333 * the mail body. It allows, for instance, to add inline images 334 * to the email. Inline files may be referenced with a 335 * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID 336 * returned by the embed function. Files are bound to their names, which is 337 * the value returned by {@link java.io.File#getName()}. If the same file 338 * is embedded multiple times, the same CID is guaranteed to be returned. 339 * 340 * <p>While functionally the same as passing <code>FileDataSource</code> to 341 * {@link #embed(DataSource, String, String)}, this method attempts 342 * to validate the file before embedding it in the message and will throw 343 * <code>EmailException</code> if the validation fails. In this case, the 344 * <code>HtmlEmail</code> object will not be changed. 345 * 346 * @param file The <code>File</code> to embed 347 * @param cid the Content-ID to use for the embedded <code>File</code> 348 * @return A String with the Content-ID of the file. 349 * @throws EmailException when the supplied <code>File</code> cannot be used 350 * or if the file has already been embedded; 351 * also see {@link javax.mail.internet.MimeBodyPart} for definitions 352 * @since 1.1 353 */ 354 public String embed(final File file, final String cid) throws EmailException 355 { 356 if (EmailUtils.isEmpty(file.getName())) 357 { 358 throw new EmailException("file name cannot be null or empty"); 359 } 360 361 // verify that the File can provide a canonical path 362 String filePath = null; 363 try 364 { 365 filePath = file.getCanonicalPath(); 366 } 367 catch (final IOException ioe) 368 { 369 throw new EmailException("couldn't get canonical path for " 370 + file.getName(), ioe); 371 } 372 373 // check if a FileDataSource for this name has already been attached; 374 // if so, return the cached CID value. 375 if (inlineEmbeds.containsKey(file.getName())) 376 { 377 final InlineImage ii = inlineEmbeds.get(file.getName()); 378 final FileDataSource fileDataSource = (FileDataSource) ii.getDataSource(); 379 // make sure the supplied file has the same canonical path 380 // as the one already associated with this name. 381 String existingFilePath = null; 382 try 383 { 384 existingFilePath = fileDataSource.getFile().getCanonicalPath(); 385 } 386 catch (final IOException ioe) 387 { 388 throw new EmailException("couldn't get canonical path for file " 389 + fileDataSource.getFile().getName() 390 + "which has already been embedded", ioe); 391 } 392 if (filePath.equals(existingFilePath)) 393 { 394 return ii.getCid(); 395 } 396 else 397 { 398 throw new EmailException("embedded name '" + file.getName() 399 + "' is already bound to file " + existingFilePath 400 + "; existing names cannot be rebound"); 401 } 402 } 403 404 // verify that the file is valid 405 if (!file.exists()) 406 { 407 throw new EmailException("file " + filePath + " doesn't exist"); 408 } 409 if (!file.isFile()) 410 { 411 throw new EmailException("file " + filePath + " isn't a normal file"); 412 } 413 if (!file.canRead()) 414 { 415 throw new EmailException("file " + filePath + " isn't readable"); 416 } 417 418 return embed(new FileDataSource(file), file.getName(), cid); 419 } 420 421 /** 422 * Embeds the specified <code>DataSource</code> in the HTML using a 423 * randomly generated Content-ID. Returns the generated Content-ID string. 424 * 425 * @param dataSource the <code>DataSource</code> to embed 426 * @param name the name that will be set in the filename header field 427 * @return the generated Content-ID for this <code>DataSource</code> 428 * @throws EmailException if the embedding fails or if <code>name</code> is 429 * null or empty 430 * @see #embed(DataSource, String, String) 431 * @since 1.1 432 */ 433 public String embed(final DataSource dataSource, final String name) throws EmailException 434 { 435 // check if the DataSource has already been attached; 436 // if so, return the cached CID value. 437 if (inlineEmbeds.containsKey(name)) 438 { 439 final InlineImage ii = inlineEmbeds.get(name); 440 // make sure the supplied URL points to the same thing 441 // as the one already associated with this name. 442 if (dataSource.equals(ii.getDataSource())) 443 { 444 return ii.getCid(); 445 } 446 else 447 { 448 throw new EmailException("embedded DataSource '" + name 449 + "' is already bound to name " + ii.getDataSource().toString() 450 + "; existing names cannot be rebound"); 451 } 452 } 453 454 final String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase(); 455 return embed(dataSource, name, cid); 456 } 457 458 /** 459 * Embeds the specified <code>DataSource</code> in the HTML using the 460 * specified Content-ID. Returns the specified Content-ID string. 461 * 462 * @param dataSource the <code>DataSource</code> to embed 463 * @param name the name that will be set in the filename header field 464 * @param cid the Content-ID to use for this <code>DataSource</code> 465 * @return the URL encoded Content-ID for this <code>DataSource</code> 466 * @throws EmailException if the embedding fails or if <code>name</code> is 467 * null or empty 468 * @since 1.1 469 */ 470 public String embed(final DataSource dataSource, final String name, String cid) 471 throws EmailException 472 { 473 if (EmailUtils.isEmpty(name)) 474 { 475 throw new EmailException("name cannot be null or empty"); 476 } 477 478 final MimeBodyPart mbp = new MimeBodyPart(); 479 480 try 481 { 482 // url encode the cid according to rfc 2392 483 cid = EmailUtils.encodeUrl(cid); 484 485 mbp.setDataHandler(new DataHandler(dataSource)); 486 mbp.setFileName(name); 487 mbp.setDisposition(EmailAttachment.INLINE); 488 mbp.setContentID("<" + cid + ">"); 489 490 final InlineImage ii = new InlineImage(cid, dataSource, mbp); 491 this.inlineEmbeds.put(name, ii); 492 493 return cid; 494 } 495 catch (final MessagingException me) 496 { 497 throw new EmailException(me); 498 } 499 catch (final UnsupportedEncodingException uee) 500 { 501 throw new EmailException(uee); 502 } 503 } 504 505 /** 506 * Does the work of actually building the MimeMessage. Please note that 507 * a user rarely calls this method directly and only if he/she is 508 * interested in the sending the underlying MimeMessage without 509 * commons-email. 510 * 511 * @exception EmailException if there was an error. 512 * @since 1.0 513 */ 514 @Override 515 public void buildMimeMessage() throws EmailException 516 { 517 try 518 { 519 build(); 520 } 521 catch (final MessagingException me) 522 { 523 throw new EmailException(me); 524 } 525 super.buildMimeMessage(); 526 } 527 528 /** 529 * @throws EmailException EmailException 530 * @throws MessagingException MessagingException 531 */ 532 private void build() throws MessagingException, EmailException 533 { 534 final MimeMultipart rootContainer = this.getContainer(); 535 MimeMultipart bodyEmbedsContainer = rootContainer; 536 MimeMultipart bodyContainer = rootContainer; 537 MimeBodyPart msgHtml = null; 538 MimeBodyPart msgText = null; 539 540 rootContainer.setSubType("mixed"); 541 542 // determine how to form multiparts of email 543 544 if (EmailUtils.isNotEmpty(this.html) && this.inlineEmbeds.size() > 0) 545 { 546 //If HTML body and embeds are used, create a related container and add it to the root container 547 bodyEmbedsContainer = new MimeMultipart("related"); 548 bodyContainer = bodyEmbedsContainer; 549 this.addPart(bodyEmbedsContainer, 0); 550 551 //If TEXT body was specified, create a alternative container and add it to the embeds container 552 if (EmailUtils.isNotEmpty(this.text)) 553 { 554 bodyContainer = new MimeMultipart("alternative"); 555 final BodyPart bodyPart = createBodyPart(); 556 try 557 { 558 bodyPart.setContent(bodyContainer); 559 bodyEmbedsContainer.addBodyPart(bodyPart, 0); 560 } 561 catch (final MessagingException me) 562 { 563 throw new EmailException(me); 564 } 565 } 566 } 567 else if (EmailUtils.isNotEmpty(this.text) && EmailUtils.isNotEmpty(this.html)) 568 { 569 //If both HTML and TEXT bodies are provided, create a alternative container and add it to the root container 570 bodyContainer = new MimeMultipart("alternative"); 571 this.addPart(bodyContainer, 0); 572 } 573 574 if (EmailUtils.isNotEmpty(this.html)) 575 { 576 msgHtml = new MimeBodyPart(); 577 bodyContainer.addBodyPart(msgHtml, 0); 578 579 // EMAIL-104: call explicitly setText to use default mime charset 580 // (property "mail.mime.charset") in case none has been set 581 msgHtml.setText(this.html, this.charset, EmailConstants.TEXT_SUBTYPE_HTML); 582 583 for (final InlineImage image : this.inlineEmbeds.values()) 584 { 585 bodyEmbedsContainer.addBodyPart(image.getMbp()); 586 } 587 } 588 589 if (EmailUtils.isNotEmpty(this.text)) 590 { 591 msgText = new MimeBodyPart(); 592 bodyContainer.addBodyPart(msgText, 0); 593 594 // EMAIL-104: call explicitly setText to use default mime charset 595 // (property "mail.mime.charset") in case none has been set 596 msgText.setText(this.text, this.charset); 597 } 598 } 599 600 /** 601 * Private bean class that encapsulates data about URL contents 602 * that are embedded in the final email. 603 * @since 1.1 604 */ 605 private static class InlineImage 606 { 607 /** content id. */ 608 private final String cid; 609 /** <code>DataSource</code> for the content. */ 610 private final DataSource dataSource; 611 /** the <code>MimeBodyPart</code> that contains the encoded data. */ 612 private final MimeBodyPart mbp; 613 614 /** 615 * Creates an InlineImage object to represent the 616 * specified content ID and <code>MimeBodyPart</code>. 617 * @param cid the generated content ID 618 * @param dataSource the <code>DataSource</code> that represents the content 619 * @param mbp the <code>MimeBodyPart</code> that contains the encoded 620 * data 621 */ 622 public InlineImage(final String cid, final DataSource dataSource, final MimeBodyPart mbp) 623 { 624 this.cid = cid; 625 this.dataSource = dataSource; 626 this.mbp = mbp; 627 } 628 629 /** 630 * Returns the unique content ID of this InlineImage. 631 * @return the unique content ID of this InlineImage 632 */ 633 public String getCid() 634 { 635 return cid; 636 } 637 638 /** 639 * Returns the <code>DataSource</code> that represents the encoded content. 640 * @return the <code>DataSource</code> representing the encoded content 641 */ 642 public DataSource getDataSource() 643 { 644 return dataSource; 645 } 646 647 /** 648 * Returns the <code>MimeBodyPart</code> that contains the 649 * encoded InlineImage data. 650 * @return the <code>MimeBodyPart</code> containing the encoded 651 * InlineImage data 652 */ 653 public MimeBodyPart getMbp() 654 { 655 return mbp; 656 } 657 658 // equals()/hashCode() implementations, since this class 659 // is stored as a entry in a Map. 660 /** 661 * {@inheritDoc} 662 * @return true if the other object is also an InlineImage with the same cid. 663 */ 664 @Override 665 public boolean equals(final Object obj) 666 { 667 if (this == obj) 668 { 669 return true; 670 } 671 if (!(obj instanceof InlineImage)) 672 { 673 return false; 674 } 675 676 final InlineImage that = (InlineImage) obj; 677 678 return this.cid.equals(that.cid); 679 } 680 681 /** 682 * {@inheritDoc} 683 * @return the cid hashCode. 684 */ 685 @Override 686 public int hashCode() 687 { 688 return cid.hashCode(); 689 } 690 } 691}