| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.commons.mail; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| import javax.activation.DataHandler; |
| import javax.activation.DataSource; |
| import javax.activation.FileDataSource; |
| import javax.activation.URLDataSource; |
| import javax.mail.BodyPart; |
| import javax.mail.MessagingException; |
| import javax.mail.internet.MimeBodyPart; |
| import javax.mail.internet.MimeMultipart; |
| |
| /** |
| * An HTML multipart email. |
| * |
| * <p>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. |
| * |
| * <p>This class also inherits from {@link MultiPartEmail}, so it is easy to |
| * add attachments to the email. |
| * |
| * <p>To send an email in HTML, one should create a <code>HtmlEmail</code>, then |
| * use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods. |
| * The HTML content can be set with the {@link #setHtmlMsg(String)} method. The |
| * alternative text content can be set with {@link #setTextMsg(String)}. |
| * |
| * <p>Either the text or HTML can be omitted, in which case the "main" |
| * part of the multipart becomes whichever is supplied rather than a |
| * <code>multipart/alternative</code>. |
| * |
| * <h3>Embedding Images and Media</h3> |
| * |
| * <p>It is also possible to embed URLs, files, or arbitrary |
| * <code>DataSource</code>s directly into the body of the mail: |
| * <pre> |
| * HtmlEmail he = new HtmlEmail(); |
| * File img = new File("my/image.gif"); |
| * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class |
| * StringBuffer msg = new StringBuffer(); |
| * msg.append("<html><body>"); |
| * msg.append("<img src=cid:").append(he.embed(img)).append(">"); |
| * msg.append("<img src=cid:").append(he.embed(png)).append(">"); |
| * msg.append("</body></html>"); |
| * he.setHtmlMsg(msg.toString()); |
| * // code to set the other email fields (not shown) |
| * </pre> |
| * |
| * <p>Embedded entities are tracked by their name, which for <code>File</code>s is |
| * the file name itself and for <code>URL</code>s is the canonical path. It is |
| * an error to bind the same name to more than one entity, and this class will |
| * attempt to validate that for <code>File</code>s and <code>URL</code>s. When |
| * embedding a <code>DataSource</code>, the code uses the <code>equals()</code> |
| * method defined on the <code>DataSource</code>s to make the determination. |
| * |
| * @since 1.0 |
| */ |
| public class HtmlEmail extends MultiPartEmail |
| { |
| /** Definition of the length of generated CID's. */ |
| public static final int CID_LENGTH = 10; |
| |
| /** prefix for default HTML mail. */ |
| private static final String HTML_MESSAGE_START = "<html><body><pre>"; |
| /** suffix for default HTML mail. */ |
| private static final String HTML_MESSAGE_END = "</pre></body></html>"; |
| |
| |
| /** |
| * Text part of the message. This will be used as alternative text if |
| * the email client does not support HTML messages. |
| */ |
| protected String text; |
| |
| /** Html part of the message. */ |
| protected String html; |
| |
| /** |
| * @deprecated As of commons-email 1.1, no longer used. Inline embedded |
| * objects are now stored in {@link #inlineEmbeds}. |
| */ |
| @Deprecated |
| protected List<InlineImage> inlineImages; |
| |
| /** |
| * Embedded images Map<String, InlineImage> where the key is the |
| * user-defined image name. |
| */ |
| protected Map<String, InlineImage> inlineEmbeds = new HashMap<String, InlineImage>(); |
| |
| /** |
| * Set the text content. |
| * |
| * @param aText A String. |
| * @return An HtmlEmail. |
| * @throws EmailException see javax.mail.internet.MimeBodyPart |
| * for definitions |
| * @since 1.0 |
| */ |
| public HtmlEmail setTextMsg(final String aText) throws EmailException |
| { |
| if (EmailUtils.isEmpty(aText)) |
| { |
| throw new EmailException("Invalid message supplied"); |
| } |
| |
| this.text = aText; |
| return this; |
| } |
| |
| /** |
| * Set the HTML content. |
| * |
| * @param aHtml A String. |
| * @return An HtmlEmail. |
| * @throws EmailException see javax.mail.internet.MimeBodyPart |
| * for definitions |
| * @since 1.0 |
| */ |
| public HtmlEmail setHtmlMsg(final String aHtml) throws EmailException |
| { |
| if (EmailUtils.isEmpty(aHtml)) |
| { |
| throw new EmailException("Invalid message supplied"); |
| } |
| |
| this.html = aHtml; |
| return this; |
| } |
| |
| /** |
| * Set the message. |
| * |
| * <p>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 message is formatted in HTML for the HTML |
| * part of the message; it is left as is in the alternate text |
| * part. |
| * |
| * @param msg the message text to use |
| * @return this <code>HtmlEmail</code> |
| * @throws EmailException if msg is null or empty; |
| * see javax.mail.internet.MimeBodyPart for definitions |
| * @since 1.0 |
| */ |
| @Override |
| public Email setMsg(final String msg) throws EmailException |
| { |
| if (EmailUtils.isEmpty(msg)) |
| { |
| throw new EmailException("Invalid message supplied"); |
| } |
| |
| setTextMsg(msg); |
| |
| final StringBuffer htmlMsgBuf = new StringBuffer( |
| msg.length() |
| + HTML_MESSAGE_START.length() |
| + HTML_MESSAGE_END.length() |
| ); |
| |
| htmlMsgBuf.append(HTML_MESSAGE_START) |
| .append(msg) |
| .append(HTML_MESSAGE_END); |
| |
| setHtmlMsg(htmlMsgBuf.toString()); |
| |
| return this; |
| } |
| |
| /** |
| * Attempts to parse the specified <code>String</code> as a URL that will |
| * then be embedded in the message. |
| * |
| * @param urlString String representation of the URL. |
| * @param name The name that will be set in the file name header field. |
| * @return A String with the Content-ID of the URL. |
| * @throws EmailException when URL supplied is invalid or if {@code name} is null |
| * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions |
| * |
| * @see #embed(URL, String) |
| * @since 1.1 |
| */ |
| public String embed(final String urlString, final String name) throws EmailException |
| { |
| try |
| { |
| return embed(new URL(urlString), name); |
| } |
| catch (final MalformedURLException e) |
| { |
| throw new EmailException("Invalid URL", e); |
| } |
| } |
| |
| /** |
| * Embeds an URL in the HTML. |
| * |
| * <p>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 referenced with a |
| * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID |
| * returned by the embed function. It is an error to bind the same name |
| * to more than one URL; if the same URL is embedded multiple times, the |
| * same Content-ID is guaranteed to be returned. |
| * |
| * <p>While functionally the same as passing <code>URLDataSource</code> to |
| * {@link #embed(DataSource, String, String)}, this method attempts |
| * to validate the URL before embedding it in the message and will throw |
| * <code>EmailException</code> if the validation fails. In this case, the |
| * <code>HtmlEmail</code> object will not be changed. |
| * |
| * <p> |
| * NOTE: Clients should take care to ensure that different URLs are bound to |
| * different names. This implementation tries to detect this and throw |
| * <code>EmailException</code>. 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 virtual host cluster. |
| * |
| * @param url The URL of the file. |
| * @param name The name that will be set in the file name header |
| * field. |
| * @return A String with the Content-ID of the file. |
| * @throws EmailException when URL supplied is invalid or if {@code name} is null |
| * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions |
| * @since 1.0 |
| */ |
| public String embed(final URL url, final String name) throws EmailException |
| { |
| if (EmailUtils.isEmpty(name)) |
| { |
| throw new EmailException("name cannot be null or empty"); |
| } |
| |
| // check if a URLDataSource for this name has already been attached; |
| // if so, return the cached CID value. |
| if (inlineEmbeds.containsKey(name)) |
| { |
| final InlineImage ii = inlineEmbeds.get(name); |
| final URLDataSource urlDataSource = (URLDataSource) ii.getDataSource(); |
| // make sure the supplied URL points to the same thing |
| // as the one already associated with this name. |
| // NOTE: Comparing URLs with URL.equals() is a blocking operation |
| // in the case of a network failure therefore we use |
| // url.toExternalForm().equals() here. |
| if (url.toExternalForm().equals(urlDataSource.getURL().toExternalForm())) |
| { |
| return ii.getCid(); |
| } |
| throw new EmailException("embedded name '" + name |
| + "' is already bound to URL " + urlDataSource.getURL() |
| + "; existing names cannot be rebound"); |
| } |
| |
| // verify that the URL is valid |
| InputStream is = null; |
| try |
| { |
| is = url.openStream(); |
| } |
| catch (final IOException e) |
| { |
| throw new EmailException("Invalid URL", e); |
| } |
| finally |
| { |
| try |
| { |
| if (is != null) |
| { |
| is.close(); |
| } |
| } |
| catch (final IOException ioe) // NOPMD |
| { /* sigh */ } |
| } |
| |
| return embed(new URLDataSource(url), name); |
| } |
| |
| /** |
| * Embeds a file in the HTML. This implementation delegates to |
| * {@link #embed(File, String)}. |
| * |
| * @param file The <code>File</code> object to embed |
| * @return A String with the Content-ID of the file. |
| * @throws EmailException when the supplied <code>File</code> cannot be |
| * used; also see {@link javax.mail.internet.MimeBodyPart} for definitions |
| * |
| * @see #embed(File, String) |
| * @since 1.1 |
| */ |
| public String embed(final File file) throws EmailException |
| { |
| final String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase(Locale.ENGLISH); |
| return embed(file, cid); |
| } |
| |
| /** |
| * Embeds a file in the HTML. |
| * |
| * <p>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 referenced with a |
| * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID |
| * returned by the embed function. Files are bound to their names, which is |
| * the value returned by {@link java.io.File#getName()}. If the same file |
| * is embedded multiple times, the same CID is guaranteed to be returned. |
| * |
| * <p>While functionally the same as passing <code>FileDataSource</code> to |
| * {@link #embed(DataSource, String, String)}, this method attempts |
| * to validate the file before embedding it in the message and will throw |
| * <code>EmailException</code> if the validation fails. In this case, the |
| * <code>HtmlEmail</code> object will not be changed. |
| * |
| * @param file The <code>File</code> to embed |
| * @param cid the Content-ID to use for the embedded <code>File</code> |
| * @return A String with the Content-ID of the file. |
| * @throws EmailException when the supplied <code>File</code> cannot be used |
| * or if the file has already been embedded; |
| * also see {@link javax.mail.internet.MimeBodyPart} for definitions |
| * @since 1.1 |
| */ |
| public String embed(final File file, final String cid) throws EmailException |
| { |
| if (EmailUtils.isEmpty(file.getName())) |
| { |
| throw new EmailException("file name cannot be null or empty"); |
| } |
| |
| // verify that the File can provide a canonical path |
| String filePath = null; |
| try |
| { |
| filePath = file.getCanonicalPath(); |
| } |
| catch (final IOException ioe) |
| { |
| throw new EmailException("couldn't get canonical path for " |
| + file.getName(), ioe); |
| } |
| |
| // check if a FileDataSource for this name has already been attached; |
| // if so, return the cached CID value. |
| if (inlineEmbeds.containsKey(file.getName())) |
| { |
| final InlineImage ii = inlineEmbeds.get(file.getName()); |
| final FileDataSource fileDataSource = (FileDataSource) ii.getDataSource(); |
| // make sure the supplied file has the same canonical path |
| // as the one already associated with this name. |
| String existingFilePath = null; |
| try |
| { |
| existingFilePath = fileDataSource.getFile().getCanonicalPath(); |
| } |
| catch (final IOException ioe) |
| { |
| throw new EmailException("couldn't get canonical path for file " |
| + fileDataSource.getFile().getName() |
| + "which has already been embedded", ioe); |
| } |
| if (filePath.equals(existingFilePath)) |
| { |
| return ii.getCid(); |
| } |
| throw new EmailException("embedded name '" + file.getName() |
| + "' is already bound to file " + existingFilePath |
| + "; existing names cannot be rebound"); |
| } |
| |
| // verify that the file is valid |
| if (!file.exists()) |
| { |
| throw new EmailException("file " + filePath + " doesn't exist"); |
| } |
| if (!file.isFile()) |
| { |
| throw new EmailException("file " + filePath + " isn't a normal file"); |
| } |
| if (!file.canRead()) |
| { |
| throw new EmailException("file " + filePath + " isn't readable"); |
| } |
| |
| return embed(new FileDataSource(file), file.getName(), cid); |
| } |
| |
| /** |
| * Embeds the specified <code>DataSource</code> in the HTML using a |
| * randomly generated Content-ID. Returns the generated Content-ID string. |
| * |
| * @param dataSource the <code>DataSource</code> to embed |
| * @param name the name that will be set in the file name header field |
| * @return the generated Content-ID for this <code>DataSource</code> |
| * @throws EmailException if the embedding fails or if <code>name</code> is |
| * null or empty |
| * @see #embed(DataSource, String, String) |
| * @since 1.1 |
| */ |
| public String embed(final DataSource dataSource, final String name) throws EmailException |
| { |
| // check if the DataSource has already been attached; |
| // if so, return the cached CID value. |
| if (inlineEmbeds.containsKey(name)) |
| { |
| final InlineImage ii = inlineEmbeds.get(name); |
| // make sure the supplied URL points to the same thing |
| // as the one already associated with this name. |
| if (dataSource.equals(ii.getDataSource())) |
| { |
| return ii.getCid(); |
| } |
| throw new EmailException("embedded DataSource '" + name |
| + "' is already bound to name " + ii.getDataSource().toString() |
| + "; existing names cannot be rebound"); |
| } |
| |
| final String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase(); |
| return embed(dataSource, name, cid); |
| } |
| |
| /** |
| * Embeds the specified <code>DataSource</code> in the HTML using the |
| * specified Content-ID. Returns the specified Content-ID string. |
| * |
| * @param dataSource the <code>DataSource</code> to embed |
| * @param name the name that will be set in the file name header field |
| * @param cid the Content-ID to use for this <code>DataSource</code> |
| * @return the URL encoded Content-ID for this <code>DataSource</code> |
| * @throws EmailException if the embedding fails or if <code>name</code> is |
| * null or empty |
| * @since 1.1 |
| */ |
| public String embed(final DataSource dataSource, final String name, final String cid) |
| throws EmailException |
| { |
| if (EmailUtils.isEmpty(name)) |
| { |
| throw new EmailException("name cannot be null or empty"); |
| } |
| |
| final MimeBodyPart mbp = new MimeBodyPart(); |
| |
| try |
| { |
| // URL encode the cid according to RFC 2392 |
| final String encodedCid = EmailUtils.encodeUrl(cid); |
| |
| mbp.setDataHandler(new DataHandler(dataSource)); |
| mbp.setFileName(name); |
| mbp.setDisposition(EmailAttachment.INLINE); |
| mbp.setContentID("<" + encodedCid + ">"); |
| |
| final InlineImage ii = new InlineImage(encodedCid, dataSource, mbp); |
| this.inlineEmbeds.put(name, ii); |
| |
| return encodedCid; |
| } |
| catch (final MessagingException me) |
| { |
| throw new EmailException(me); |
| } |
| catch (final UnsupportedEncodingException uee) |
| { |
| throw new EmailException(uee); |
| } |
| } |
| |
| /** |
| * Does the work of actually building the MimeMessage. Please note that |
| * a user rarely calls this method directly and only if he/she is |
| * interested in the sending the underlying MimeMessage without |
| * commons-email. |
| * |
| * @throws EmailException if there was an error. |
| * @since 1.0 |
| */ |
| @Override |
| public void buildMimeMessage() throws EmailException |
| { |
| try |
| { |
| build(); |
| } |
| catch (final MessagingException me) |
| { |
| throw new EmailException(me); |
| } |
| super.buildMimeMessage(); |
| } |
| |
| /** |
| * @throws EmailException EmailException |
| * @throws MessagingException MessagingException |
| */ |
| private void build() throws MessagingException, EmailException |
| { |
| final MimeMultipart rootContainer = this.getContainer(); |
| MimeMultipart bodyEmbedsContainer = rootContainer; |
| MimeMultipart bodyContainer = rootContainer; |
| MimeBodyPart msgHtml = null; |
| MimeBodyPart msgText = null; |
| |
| rootContainer.setSubType("mixed"); |
| |
| // determine how to form multiparts of email |
| |
| if (EmailUtils.isNotEmpty(this.html) && this.inlineEmbeds.size() > 0) |
| { |
| //If HTML body and embeds are used, create a related container and add it to the root container |
| bodyEmbedsContainer = new MimeMultipart("related"); |
| bodyContainer = bodyEmbedsContainer; |
| this.addPart(bodyEmbedsContainer, 0); |
| |
| // If TEXT body was specified, create a alternative container and add it to the embeds container |
| if (EmailUtils.isNotEmpty(this.text)) |
| { |
| bodyContainer = new MimeMultipart("alternative"); |
| final BodyPart bodyPart = createBodyPart(); |
| try |
| { |
| bodyPart.setContent(bodyContainer); |
| bodyEmbedsContainer.addBodyPart(bodyPart, 0); |
| } |
| catch (final MessagingException me) |
| { |
| throw new EmailException(me); |
| } |
| } |
| } |
| else if (EmailUtils.isNotEmpty(this.text) && EmailUtils.isNotEmpty(this.html)) |
| { |
| // EMAIL-142: if we have both an HTML and TEXT body, but no attachments or |
| // inline images, the root container should have mimetype |
| // "multipart/alternative". |
| // reference: http://tools.ietf.org/html/rfc2046#section-5.1.4 |
| if (this.inlineEmbeds.size() > 0 || isBoolHasAttachments()) |
| { |
| // If both HTML and TEXT bodies are provided, create an alternative |
| // container and add it to the root container |
| bodyContainer = new MimeMultipart("alternative"); |
| this.addPart(bodyContainer, 0); |
| } |
| else |
| { |
| // no attachments or embedded images present, change the mimetype |
| // of the root container (= body container) |
| rootContainer.setSubType("alternative"); |
| } |
| } |
| |
| if (EmailUtils.isNotEmpty(this.html)) |
| { |
| msgHtml = new MimeBodyPart(); |
| bodyContainer.addBodyPart(msgHtml, 0); |
| |
| // EMAIL-104: call explicitly setText to use default mime charset |
| // (property "mail.mime.charset") in case none has been set |
| msgHtml.setText(this.html, this.charset, EmailConstants.TEXT_SUBTYPE_HTML); |
| |
| // EMAIL-147: work-around for buggy JavaMail implementations; |
| // in case setText(...) does not set the correct content type, |
| // use the setContent() method instead. |
| final String contentType = msgHtml.getContentType(); |
| if (contentType == null || !contentType.equals(EmailConstants.TEXT_HTML)) |
| { |
| // apply default charset if one has been set |
| if (EmailUtils.isNotEmpty(this.charset)) |
| { |
| msgHtml.setContent(this.html, EmailConstants.TEXT_HTML + "; charset=" + this.charset); |
| } |
| else |
| { |
| // unfortunately, MimeUtility.getDefaultMIMECharset() is package private |
| // and thus can not be used to set the default system charset in case |
| // no charset has been provided by the user |
| msgHtml.setContent(this.html, EmailConstants.TEXT_HTML); |
| } |
| } |
| |
| for (final InlineImage image : this.inlineEmbeds.values()) |
| { |
| bodyEmbedsContainer.addBodyPart(image.getMbp()); |
| } |
| } |
| |
| if (EmailUtils.isNotEmpty(this.text)) |
| { |
| msgText = new MimeBodyPart(); |
| bodyContainer.addBodyPart(msgText, 0); |
| |
| // EMAIL-104: call explicitly setText to use default mime charset |
| // (property "mail.mime.charset") in case none has been set |
| msgText.setText(this.text, this.charset); |
| } |
| } |
| |
| /** |
| * Private bean class that encapsulates data about URL contents |
| * that are embedded in the final email. |
| * @since 1.1 |
| */ |
| private static class InlineImage |
| { |
| /** content id. */ |
| private final String cid; |
| /** <code>DataSource</code> for the content. */ |
| private final DataSource dataSource; |
| /** the <code>MimeBodyPart</code> that contains the encoded data. */ |
| private final MimeBodyPart mbp; |
| |
| /** |
| * Creates an InlineImage object to represent the |
| * specified content ID and <code>MimeBodyPart</code>. |
| * @param cid the generated content ID |
| * @param dataSource the <code>DataSource</code> that represents the content |
| * @param mbp the <code>MimeBodyPart</code> that contains the encoded |
| * data |
| */ |
| public InlineImage(final String cid, final DataSource dataSource, final MimeBodyPart mbp) |
| { |
| this.cid = cid; |
| this.dataSource = dataSource; |
| this.mbp = mbp; |
| } |
| |
| /** |
| * Returns the unique content ID of this InlineImage. |
| * @return the unique content ID of this InlineImage |
| */ |
| public String getCid() |
| { |
| return cid; |
| } |
| |
| /** |
| * Returns the <code>DataSource</code> that represents the encoded content. |
| * @return the <code>DataSource</code> representing the encoded content |
| */ |
| public DataSource getDataSource() |
| { |
| return dataSource; |
| } |
| |
| /** |
| * Returns the <code>MimeBodyPart</code> that contains the |
| * encoded InlineImage data. |
| * @return the <code>MimeBodyPart</code> containing the encoded |
| * InlineImage data |
| */ |
| public MimeBodyPart getMbp() |
| { |
| return mbp; |
| } |
| |
| // equals()/hashCode() implementations, since this class |
| // is stored as a entry in a Map. |
| /** |
| * {@inheritDoc} |
| * @return true if the other object is also an InlineImage with the same cid. |
| */ |
| @Override |
| public boolean equals(final Object obj) |
| { |
| if (this == obj) |
| { |
| return true; |
| } |
| if (!(obj instanceof InlineImage)) |
| { |
| return false; |
| } |
| |
| final InlineImage that = (InlineImage) obj; |
| |
| return this.cid.equals(that.cid); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * @return the cid hashCode. |
| */ |
| @Override |
| public int hashCode() |
| { |
| return cid.hashCode(); |
| } |
| } |
| } |