1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.mail; 18 19 import java.io.File; 20 import java.io.IOException; 21 import java.io.InputStream; 22 import java.net.MalformedURLException; 23 import java.net.URL; 24 import java.util.HashMap; 25 import java.util.Iterator; 26 import java.util.List; 27 import java.util.Map; 28 29 import javax.activation.DataHandler; 30 import javax.activation.DataSource; 31 import javax.activation.FileDataSource; 32 import javax.activation.URLDataSource; 33 import javax.mail.BodyPart; 34 import javax.mail.MessagingException; 35 import javax.mail.internet.MimeBodyPart; 36 import javax.mail.internet.MimeMultipart; 37 38 /** 39 * An HTML multipart email. 40 * 41 * <p>This class is used to send HTML formatted email. A text message 42 * can also be set for HTML unaware email clients, such as text-based 43 * email clients. 44 * 45 * <p>This class also inherits from {@link MultiPartEmail}, so it is easy to 46 * add attachments to the email. 47 * 48 * <p>To send an email in HTML, one should create a <code>HtmlEmail</code>, then 49 * use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods. 50 * The HTML content can be set with the {@link #setHtmlMsg(String)} method. The 51 * alternative text content can be set with {@link #setTextMsg(String)}. 52 * 53 * <p>Either the text or HTML can be omitted, in which case the "main" 54 * part of the multipart becomes whichever is supplied rather than a 55 * <code>multipart/alternative</code>. 56 * 57 * <h3>Embedding Images and Media</h3> 58 * 59 * <p>It is also possible to embed URLs, files, or arbitrary 60 * <code>DataSource</code>s directly into the body of the mail: 61 * <pre><code> 62 * HtmlEmail he = new HtmlEmail(); 63 * File img = new File("my/image.gif"); 64 * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class 65 * StringBuffer msg = new StringBuffer(); 66 * msg.append("<html><body>"); 67 * msg.append("<img src=cid:").append(he.embed(img)).append(">"); 68 * msg.append("<img src=cid:").append(he.embed(png)).append(">"); 69 * msg.append("</body></html>"); 70 * he.setHtmlMsg(msg.toString()); 71 * // code to set the other email fields (not shown) 72 * </pre></code> 73 * 74 * <p>Embedded entities are tracked by their name, which for <code>File</code>s is 75 * the filename itself and for <code>URL</code>s is the canonical path. It is 76 * an error to bind the same name to more than one entity, and this class will 77 * attempt to validate that for <code>File</code>s and <code>URL</code>s. When 78 * embedding a <code>DataSource</code>, the code uses the <code>equals()</code> 79 * method defined on the <code>DataSource</code>s to make the determination. 80 * 81 * @since 1.0 82 * @author <a href="mailto:unknown">Regis Koenig</a> 83 * @author <a href="mailto:sean@informage.net">Sean Legassick</a> 84 * @version $Id: HtmlEmail.java 578501 2007-09-22 21:18:49Z bspeakmon $ 85 */ 86 public class HtmlEmail extends MultiPartEmail 87 { 88 /** Definition of the length of generated CID's */ 89 public static final int CID_LENGTH = 10; 90 91 /** prefix for default HTML mail */ 92 private static final String HTML_MESSAGE_START = "<html><body><pre>"; 93 /** suffix for default HTML mail */ 94 private static final String HTML_MESSAGE_END = "</pre></body></html>"; 95 96 97 /** 98 * Text part of the message. This will be used as alternative text if 99 * the email client does not support HTML messages. 100 */ 101 protected String text; 102 103 /** Html part of the message */ 104 protected String html; 105 106 /** 107 * @deprecated As of commons-email 1.1, no longer used. Inline embedded 108 * objects are now stored in {@link #inlineEmbeds}. 109 */ 110 protected List inlineImages; 111 112 /** 113 * Embedded images Map<String, InlineImage> where the key is the 114 * user-defined image name. 115 */ 116 protected Map inlineEmbeds = new HashMap(); 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(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(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 public Email setMsg(String msg) throws EmailException 174 { 175 if (EmailUtils.isEmpty(msg)) 176 { 177 throw new EmailException("Invalid message supplied"); 178 } 179 180 setTextMsg(msg); 181 182 StringBuffer htmlMsgBuf = new StringBuffer( 183 msg.length() 184 + HTML_MESSAGE_START.length() 185 + HTML_MESSAGE_END.length() 186 ); 187 188 htmlMsgBuf.append(HTML_MESSAGE_START) 189 .append(msg) 190 .append(HTML_MESSAGE_END); 191 192 setHtmlMsg(htmlMsgBuf.toString()); 193 194 return this; 195 } 196 197 /** 198 * Attempts to parse the specified <code>String</code> as a URL that will 199 * then be embedded in the message. 200 * 201 * @param urlString String representation of the URL. 202 * @param name The name that will be set in the filename header field. 203 * @return A String with the Content-ID of the URL. 204 * @throws EmailException when URL supplied is invalid or if <code> is null 205 * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions 206 * 207 * @see #embed(URL, String) 208 * @since 1.1 209 */ 210 public String embed(String urlString, String name) throws EmailException 211 { 212 try 213 { 214 return embed(new URL(urlString), name); 215 } 216 catch (MalformedURLException e) 217 { 218 throw new EmailException("Invalid URL", e); 219 } 220 } 221 222 /** 223 * Embeds an URL in the HTML. 224 * 225 * <p>This method embeds a file located by an URL into 226 * the mail body. It allows, for instance, to add inline images 227 * to the email. Inline files may be referenced with a 228 * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID 229 * returned by the embed function. It is an error to bind the same name 230 * to more than one URL; if the same URL is embedded multiple times, the 231 * same Content-ID is guaranteed to be returned. 232 * 233 * <p>While functionally the same as passing <code>URLDataSource</code> to 234 * {@link #embed(DataSource, String, String)}, this method attempts 235 * to validate the URL before embedding it in the message and will throw 236 * <code>EmailException</code> if the validation fails. In this case, the 237 * <code>HtmlEmail</code> object will not be changed. 238 * 239 * <p> 240 * NOTE: Clients should take care to ensure that different URLs are bound to 241 * different names. This implementation tries to detect this and throw 242 * <code>EmailException</code>. However, it is not guaranteed to catch 243 * all cases, especially when the URL refers to a remote HTTP host that 244 * may be part of a virtual host cluster. 245 * 246 * @param url The URL of the file. 247 * @param name The name that will be set in the filename header 248 * field. 249 * @return A String with the Content-ID of the file. 250 * @throws EmailException when URL supplied is invalid or if <code> is null 251 * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions 252 * @since 1.0 253 */ 254 public String embed(URL url, String name) throws EmailException 255 { 256 if (EmailUtils.isEmpty(name)) 257 { 258 throw new EmailException("name cannot be null or empty"); 259 } 260 261 // check if a URLDataSource for this name has already been attached; 262 // if so, return the cached CID value. 263 if (inlineEmbeds.containsKey(name)) 264 { 265 InlineImage ii = (InlineImage) inlineEmbeds.get(name); 266 URLDataSource urlDataSource = (URLDataSource) ii.getDataSource(); 267 // make sure the supplied URL points to the same thing 268 // as the one already associated with this name. 269 if (url.equals(urlDataSource.getURL())) 270 { 271 return ii.getCid(); 272 } 273 else 274 { 275 throw new EmailException("embedded name '" + name 276 + "' is already bound to URL " + urlDataSource.getURL() 277 + "; existing names cannot be rebound"); 278 } 279 // NOTE: Comparing URLs with URL.equals() is known to be 280 // inconsistent when dealing with virtual hosting over HTTP, 281 // but since these are almost always files on the local machine, 282 // using equals() should be sufficient. 283 } 284 285 // verify that the URL is valid 286 InputStream is = null; 287 try 288 { 289 is = url.openStream(); 290 } 291 catch (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 (IOException ioe) 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(File file) throws EmailException 324 { 325 String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase(); 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(File file, 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 (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 InlineImage ii = (InlineImage) inlineEmbeds.get(file.getName()); 378 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 (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()); 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(DataSource dataSource, 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 InlineImage ii = (InlineImage) 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 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 supplied 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(DataSource dataSource, 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 MimeBodyPart mbp = new MimeBodyPart(); 479 480 try 481 { 482 mbp.setDataHandler(new DataHandler(dataSource)); 483 mbp.setFileName(name); 484 mbp.setDisposition("inline"); 485 mbp.setContentID("<" + cid + ">"); 486 487 InlineImage ii = new InlineImage(cid, dataSource, mbp); 488 this.inlineEmbeds.put(name, ii); 489 490 return cid; 491 } 492 catch (MessagingException me) 493 { 494 throw new EmailException(me); 495 } 496 } 497 498 /** 499 * Does the work of actually building the email. 500 * 501 * @exception EmailException if there was an error. 502 * @since 1.0 503 */ 504 public void buildMimeMessage() throws EmailException 505 { 506 try 507 { 508 build(); 509 } 510 catch (MessagingException me) 511 { 512 throw new EmailException(me); 513 } 514 super.buildMimeMessage(); 515 } 516 517 /** 518 * @throws EmailException EmailException 519 * @throws MessagingException MessagingException 520 */ 521 private void build() throws MessagingException, EmailException 522 { 523 MimeMultipart container = this.getContainer(); 524 MimeMultipart subContainer = null; 525 BodyPart msgHtml = null; 526 BodyPart msgText = null; 527 528 container.setSubType("related"); 529 subContainer = new MimeMultipart("alternative"); 530 531 if (EmailUtils.isNotEmpty(this.text)) 532 { 533 msgText = new MimeBodyPart(); 534 if (this.inlineEmbeds.size() > 0) 535 { 536 subContainer.addBodyPart(msgText); 537 } 538 else 539 { 540 container.addBodyPart(msgText); 541 } 542 543 // apply default charset if one has been set 544 if (EmailUtils.isNotEmpty(this.charset)) 545 { 546 msgText.setContent( 547 this.text, 548 Email.TEXT_PLAIN + "; charset=" + this.charset); 549 } 550 else 551 { 552 msgText.setContent(this.text, Email.TEXT_PLAIN); 553 } 554 } 555 556 if (EmailUtils.isNotEmpty(this.html)) 557 { 558 msgHtml = new MimeBodyPart(); 559 if (this.inlineEmbeds.size() > 0) 560 { 561 subContainer.addBodyPart(msgHtml); 562 } 563 else 564 { 565 container.addBodyPart(msgHtml); 566 } 567 568 // apply default charset if one has been set 569 if (EmailUtils.isNotEmpty(this.charset)) 570 { 571 msgHtml.setContent( 572 this.html, 573 Email.TEXT_HTML + "; charset=" + this.charset); 574 } 575 else 576 { 577 msgHtml.setContent(this.html, Email.TEXT_HTML); 578 } 579 580 Iterator iter = this.inlineEmbeds.values().iterator(); 581 while (iter.hasNext()) 582 { 583 InlineImage ii = (InlineImage) iter.next(); 584 container.addBodyPart(ii.getMbp()); 585 } 586 } 587 588 if (this.inlineEmbeds.size() > 0) 589 { 590 // add sub container to message 591 this.addPart(subContainer, 0); 592 } 593 } 594 595 /** 596 * Private bean class that encapsulates data about URL contents 597 * that are embedded in the final email. 598 * @since 1.1 599 */ 600 private static class InlineImage 601 { 602 /** content id */ 603 private String cid; 604 /** <code>DataSource</code> for the content */ 605 private DataSource dataSource; 606 /** the <code>MimeBodyPart</code> that contains the encoded data */ 607 private MimeBodyPart mbp; 608 609 /** 610 * Creates an InlineImage object to represent the 611 * specified content ID and <code>MimeBodyPart</code>. 612 * @param cid the generated content ID 613 * @param dataSource the <code>DataSource</code> that represents the content 614 * @param mbp the <code>MimeBodyPart</code> that contains the encoded 615 * data 616 */ 617 public InlineImage(String cid, DataSource dataSource, MimeBodyPart mbp) 618 { 619 this.cid = cid; 620 this.dataSource = dataSource; 621 this.mbp = mbp; 622 } 623 624 /** 625 * Returns the unique content ID of this InlineImage. 626 * @return the unique content ID of this InlineImage 627 */ 628 public String getCid() 629 { 630 return cid; 631 } 632 633 /** 634 * Returns the <code>DataSource</code> that represents the encoded content. 635 * @return the <code>DataSource</code> representing the encoded content 636 */ 637 public DataSource getDataSource() 638 { 639 return dataSource; 640 } 641 642 /** 643 * Returns the <code>MimeBodyPart</code> that contains the 644 * encoded InlineImage data. 645 * @return the <code>MimeBodyPart</code> containing the encoded 646 * InlineImage data 647 */ 648 public MimeBodyPart getMbp() 649 { 650 return mbp; 651 } 652 653 // equals()/hashCode() implementations, since this class 654 // is stored as a entry in a Map. 655 /** 656 * {@inheritDoc} 657 * @return true if the other object is also an InlineImage with the same cid. 658 */ 659 public boolean equals(Object obj) 660 { 661 if (this == obj) 662 { 663 return true; 664 } 665 if (!(obj instanceof InlineImage)) 666 { 667 return false; 668 } 669 670 InlineImage that = (InlineImage) obj; 671 672 return this.cid.equals(that.cid); 673 } 674 675 /** 676 * {@inheritDoc} 677 * @return the cid hashCode. 678 */ 679 public int hashCode() 680 { 681 return cid.hashCode(); 682 } 683 } 684 }