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 */
017 package org.apache.commons.mail;
018
019 import java.io.File;
020 import java.io.IOException;
021 import java.io.InputStream;
022 import java.net.MalformedURLException;
023 import java.net.URL;
024 import java.util.HashMap;
025 import java.util.Iterator;
026 import java.util.List;
027 import java.util.Map;
028
029 import javax.activation.DataHandler;
030 import javax.activation.DataSource;
031 import javax.activation.FileDataSource;
032 import javax.activation.URLDataSource;
033 import javax.mail.BodyPart;
034 import javax.mail.MessagingException;
035 import javax.mail.internet.MimeBodyPart;
036 import javax.mail.internet.MimeMultipart;
037
038 /**
039 * An HTML multipart email.
040 *
041 * <p>This class is used to send HTML formatted email. A text message
042 * can also be set for HTML unaware email clients, such as text-based
043 * email clients.
044 *
045 * <p>This class also inherits from {@link MultiPartEmail}, so it is easy to
046 * add attachments to the email.
047 *
048 * <p>To send an email in HTML, one should create a <code>HtmlEmail</code>, then
049 * use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods.
050 * The HTML content can be set with the {@link #setHtmlMsg(String)} method. The
051 * alternative text content can be set with {@link #setTextMsg(String)}.
052 *
053 * <p>Either the text or HTML can be omitted, in which case the "main"
054 * part of the multipart becomes whichever is supplied rather than a
055 * <code>multipart/alternative</code>.
056 *
057 * <h3>Embedding Images and Media</h3>
058 *
059 * <p>It is also possible to embed URLs, files, or arbitrary
060 * <code>DataSource</code>s directly into the body of the mail:
061 * <pre><code>
062 * HtmlEmail he = new HtmlEmail();
063 * File img = new File("my/image.gif");
064 * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class
065 * StringBuffer msg = new StringBuffer();
066 * msg.append("<html><body>");
067 * msg.append("<img src=cid:").append(he.embed(img)).append(">");
068 * msg.append("<img src=cid:").append(he.embed(png)).append(">");
069 * msg.append("</body></html>");
070 * he.setHtmlMsg(msg.toString());
071 * // code to set the other email fields (not shown)
072 * </pre></code>
073 *
074 * <p>Embedded entities are tracked by their name, which for <code>File</code>s is
075 * the filename itself and for <code>URL</code>s is the canonical path. It is
076 * an error to bind the same name to more than one entity, and this class will
077 * attempt to validate that for <code>File</code>s and <code>URL</code>s. When
078 * embedding a <code>DataSource</code>, the code uses the <code>equals()</code>
079 * method defined on the <code>DataSource</code>s to make the determination.
080 *
081 * @since 1.0
082 * @author <a href="mailto:unknown">Regis Koenig</a>
083 * @author <a href="mailto:sean@informage.net">Sean Legassick</a>
084 * @version $Id: HtmlEmail.java 578501 2007-09-22 21:18:49Z bspeakmon $
085 */
086 public class HtmlEmail extends MultiPartEmail
087 {
088 /** Definition of the length of generated CID's */
089 public static final int CID_LENGTH = 10;
090
091 /** prefix for default HTML mail */
092 private static final String HTML_MESSAGE_START = "<html><body><pre>";
093 /** suffix for default HTML mail */
094 private static final String HTML_MESSAGE_END = "</pre></body></html>";
095
096
097 /**
098 * Text part of the message. This will be used as alternative text if
099 * 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 }