/*
 *
 *    OPEN-XCHANGE legal information
 *
 *    All intellectual property rights in the Software are protected by
 *    international copyright laws.
 *
 *
 *    In some countries OX, OX Open-Xchange, open xchange and OXtender
 *    as well as the corresponding Logos OX Open-Xchange and OX are registered
 *    trademarks of the Open-Xchange, Inc. group of companies.
 *    The use of the Logos is not covered by the GNU General Public License.
 *    Instead, you are allowed to use these Logos according to the terms and
 *    conditions of the Creative Commons License, Version 2.5, Attribution,
 *    Non-commercial, ShareAlike, and the interpretation of the term
 *    Non-commercial applicable to the aforementioned license is published
 *    on the web site http://www.open-xchange.com/EN/legal/index.html.
 *
 *    Please make sure that third-party modules and libraries are used
 *    according to their respective licenses.
 *
 *    Any modifications to this package must retain all copyright notices
 *    of the original copyright holder(s) for the original code used.
 *
 *    After any such modifications, the original and derivative code shall remain
 *    under the copyright of the copyright holder(s) and/or original author(s)per
 *    the Attribution and Assignment Agreement that can be located at
 *    http://www.open-xchange.com/EN/developer/. The contributing author shall be
 *    given Attribution for the derivative code and a license granting use.
 *
 *     Copyright (C) 2004-2010 Open-Xchange, Inc.
 *     Mail: info@open-xchange.com
 *
 *
 *     This program is free software; you can redistribute it and/or modify it
 *     under the terms of the GNU General Public License, Version 2 as published
 *     by the Free Software Foundation.
 *
 *     This program is distributed in the hope that it will be useful, but
 *     WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 *     or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 *     for more details.
 *
 *     You should have received a copy of the GNU General Public License along
 *     with this program; if not, write to the Free Software Foundation, Inc., 59
 *     Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 */

package com.openexchange.mail.text;

import static com.openexchange.mail.utils.MailFolderUtility.prepareFullname;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import com.openexchange.conversion.DataArguments;
import com.openexchange.html.HTMLService;
import com.openexchange.html.Range;
import com.openexchange.image.ImageService;
import com.openexchange.mail.MailPath;
import com.openexchange.mail.config.MailProperties;
import com.openexchange.mail.conversion.InlineImageDataSource;
import com.openexchange.mail.mime.ContentType;
import com.openexchange.mail.usersetting.UserSettingMail;
import com.openexchange.mail.utils.DisplayMode;
import com.openexchange.server.services.ServerServiceRegistry;
import com.openexchange.session.Session;
import com.openexchange.tools.regex.MatcherReplacer;

/**
 * {@link HTMLProcessing} - Various methods for HTML processing.
 * 
 * @author <a href="mailto:thorben.betten@open-xchange.com">Thorben Betten</a>
 */
public final class HTMLProcessing {
    
    private static final org.apache.commons.logging.Log LOG = org.apache.commons.logging.LogFactory.getLog(HTMLProcessing.class);

    private static final String CHARSET_US_ASCII = "US-ASCII";

    /**
     * Performs all the formatting for text content for a proper display according to specified user's mail settings.
     * 
     * @param content The plain text content
     * @param usm The settings used for formatting content
     * @param mode The display mode
     * @see #formatContentForDisplay(String, String, boolean, Session, MailPath, UserSettingMail, boolean[], DisplayMode)
     * @return The formatted content
     */
    public static String formatTextForDisplay(final String content, final UserSettingMail usm, final DisplayMode mode) {
        return formatContentForDisplay(content, null, false, null, null, usm, null, mode);
    }

    /**
     * Performs all the formatting for HTML content for a proper display according to specified user's mail settings.
     * 
     * @param content The HTML content
     * @param charset The character encoding
     * @param session The session
     * @param mailPath The message's unique path in mailbox
     * @param usm The settings used for formatting content
     * @param modified A <code>boolean</code> array with length <code>1</code> to store modified status of external images filter
     * @param mode The display mode
     * @see #formatContentForDisplay(String, String, boolean, Session, MailPath, UserSettingMail, boolean[], DisplayMode)
     * @return The formatted content
     */
    public static String formatHTMLForDisplay(final String content, final String charset, final Session session, final MailPath mailPath, final UserSettingMail usm, final boolean[] modified, final DisplayMode mode) {
        return formatContentForDisplay(content, charset, true, session, mailPath, usm, modified, mode);
    }

    /**
     * Performs all the formatting for both text and HTML content for a proper display according to specified user's mail settings.
     * <p>
     * If content is <b>plain text</b>:<br>
     * <ol>
     * <li>Plain text content is converted to valid HTML if at least {@link DisplayMode#MODIFYABLE} is given</li>
     * <li>If enabled by settings simple quotes are turned to colored block quotes if {@link DisplayMode#DISPLAY} is given</li>
     * <li>HTML links and URLs found in content are going to be prepared for proper display if {@link DisplayMode#DISPLAY} is given</li>
     * </ol>
     * If content is <b>HTML</b>:<br>
     * <ol>
     * <li>Both inline and non-inline images found in HTML content are prepared according to settings if {@link DisplayMode#DISPLAY} is
     * given</li>
     * </ol>
     * 
     * @param content The content
     * @param charset The character encoding (only needed by HTML content; may be <code>null</code> on plain text)
     * @param isHtml <code>true</code> if content is of type <code>text/html</code>; otherwise <code>false</code>
     * @param session The session
     * @param mailPath The message's unique path in mailbox
     * @param usm The settings used for formatting content
     * @param modified A <code>boolean</code> array with length <code>1</code> to store modified status of external images filter (only
     *            needed by HTML content; may be <code>null</code> on plain text)
     * @param mode The display mode
     * @return The formatted content
     */
    public static String formatContentForDisplay(final String content, final String charset, final boolean isHtml, final Session session, final MailPath mailPath, final UserSettingMail usm, final boolean[] modified, final DisplayMode mode) {
        String retval = null;
        final HTMLService htmlService = ServerServiceRegistry.getInstance().getService(HTMLService.class);
        if (isHtml) {
            retval = htmlService.getConformHTML(content, charset == null ? CHARSET_US_ASCII : charset, false);
            if (DisplayMode.MODIFYABLE.isIncluded(mode) && usm.isDisplayHtmlInlineContent()) {
                /*
                 * Filter according to white-list
                 */
                retval = htmlService.filterWhitelist(retval);
                
                if (!usm.isAllowHTMLImages()) {
                    retval = htmlService.filterExternalImages(retval, modified);
                }
                /*
                 * Filter inlined images
                 */
                if (mailPath != null && session != null) {
                    retval = filterInlineImages(retval, session, mailPath);
                }
            }
            // if (DisplayMode.DISPLAY.equals(mode) && usm.isDisplayHtmlInlineContent()) {
            // if (!usm.isAllowHTMLImages()) {
            // retval = filterExternalImages(retval, modified);
            // }
            // if (mailPath != null && session != null) {
            // retval = filterInlineImages(retval, session, mailPath);
            // }
            // }
        } else {
            retval = content;
            if (DisplayMode.MODIFYABLE.isIncluded(mode)) {
                if (DisplayMode.DISPLAY.equals(mode)) {
                    retval = htmlService.formatURLs(retval);
                    retval = htmlService.htmlFormat(retval, true, getAnchorPositions(retval));
                    if (usm.isUseColorQuote()) {
                        retval = replaceHTMLSimpleQuotesForDisplay(retval);
                    }
                } else {
                    retval = htmlService.htmlFormat(retval);
                }
            }
        }
        return retval;
    }

    /**
     * The regular expression to match links inside both plain text and HTML content.
     * <p>
     * <b>WARNING</b>: May throw a {@link StackOverflowError} if a matched link is too large. Usages should handle this case. public static
     * final Pattern PATTERN_HREF = Pattern.compile("<a\\s+href[^>]+>.*?</a>|((?:https?://|ftp://|mailto:|news\\.|www\\.)(?:[-\\p{L}0-9+@#/%?=~_|!:,.;]|&amp;|&(?![\\p{L}_0-9]+;))*(?:[-\\p{L}0-9+@#/%=~_|]|&amp;|&(?![\\p{L}_0-9]+;)))"
     * , Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
     */

    private static final String REGEX_ANCHOR = "<a\\s+href[^>]+>.*?</a>";

    private static final Pattern PATTERN_ANCHOR = Pattern.compile(REGEX_ANCHOR);

    private static List<Range> getAnchorPositions(final String content) {
        try {
            final Matcher m = PATTERN_ANCHOR.matcher(content);
            if (m.find()) {
                final List<Range> positions = new ArrayList<Range>();
                do {
                    positions.add(new Range(m.start(), m.end()));
                } while (m.find());
                return positions;
            }
        } catch (final Exception e) {
            LOG.error(e.getMessage(), e);
        } catch (final StackOverflowError error) {
            LOG.error(StackOverflowError.class.getName(), error);
        }
        return Collections.emptyList();
    }

    /**
     * Converts specified HTML content to plain text.
     * 
     * @param htmlContent The <b>validated</b> HTML content
     * @param appendHref <code>true</code> to append URLs contained in <i>href</i>s and <i>src</i>s; otherwise <code>false</code>.<br>
     *            Example: <code>&lt;a&nbsp;href=\"www.somewhere.com\"&gt;Link&lt;a&gt;</code> would be
     *            <code>Link&nbsp;[www.somewhere.com]</code>
     * @return The plain text representation of specified HTML content
     */
    public static String html2text(String htmlContent, boolean appendHref) {
        return ServerServiceRegistry.getInstance().getService(HTMLService.class).html2text(htmlContent, appendHref);
    }

    /**
     * Searches for non-HTML links and convert them to valid HTML links.
     * <p>
     * Example: <code>http://www.somewhere.com</code> is converted to
     * <code>&lt;a&nbsp;href=&quot;http://www.somewhere.com&quot;&gt;http://www.somewhere.com&lt;/a&gt;</code>.
     * 
     * @param content The content to search in
     * @return The given content with all non-HTML links converted to valid HTML links
     */
    public static String formatHrefLinks(final String content) {
        return ServerServiceRegistry.getInstance().getService(HTMLService.class).formatHrefLinks(content);
    }

    /**
     * Creates valid HTML from specified HTML content conform to W3C standards.
     * 
     * @param htmlContent The HTML content
     * @param contentType The corresponding content type (including charset parameter)
     * @return The HTML content conform to W3C standards
     */
    public static String getConformHTML(final String htmlContent, final ContentType contentType) {
        return getConformHTML(htmlContent, contentType.getCharsetParameter());
    }

    /**
     * Creates valid HTML from specified HTML content conform to W3C standards.
     * 
     * @param htmlContent The HTML content
     * @param charset The charset parameter
     * @return The HTML content conform to W3C standards
     */
    public static String getConformHTML(final String htmlContent, final String charset) {
        return ServerServiceRegistry.getInstance().getService(HTMLService.class).getConformHTML(htmlContent, charset);
    }

    /**
     * Creates a {@link Document DOM document} from specified XML/HTML string.
     * 
     * @param string The XML/HTML string
     * @return A newly created DOM document or <code>null</code> if given string cannot be transformed to a DOM document
     */
    public static Document createDOMDocument(final String string) {
        try {
            return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(string)));
        } catch (final ParserConfigurationException e) {
            LOG.error(e.getMessage(), e);
        } catch (final SAXException e) {
            LOG.error(e.getMessage(), e);
        } catch (final IOException e) {
            LOG.error(e.getMessage(), e);
        } catch (final Exception e) {
            LOG.error(e.getMessage(), e);
        }
        return null;
    }

    /**
     * Pretty-prints specified XML/HTML string.
     * 
     * @param string The XML/HTML string to pretty-print
     * @return The pretty-printed XML/HTML string
     */
    public static String prettyPrintXML(final String string) {
        return prettyPrintXML(createDOMDocument(string), string);
    }

    /**
     * Pretty-prints specified XML/HTML node.
     * 
     * @param node The XML/HTML node pretty-print
     * @return The pretty-printed XML/HTML node
     */
    public static String prettyPrintXML(final Node node) {
        return prettyPrintXML(node, null);
    }

    private static final int INDENT = 2;

    /**
     * Pretty-prints specified XML/HTML string.
     * 
     * @param node The XML/HTML node pretty-print
     * @param fallback The fallback string to return on error
     * @return The pretty-printed XML/HTML string
     */
    private static String prettyPrintXML(final Node node, final String fallback) {
        if (null == node) {
            return fallback;
        }
        /*
         * Pretty-print using Transformer
         */
        final TransformerFactory tfactory = TransformerFactory.newInstance();
        try {
            tfactory.setAttribute("indent-number", new Integer(INDENT));
            final Transformer serializer = tfactory.newTransformer();
            /*
             * Setup indenting to "pretty print"
             */
            serializer.setOutputProperty(OutputKeys.INDENT, "yes");
            serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(INDENT));
            final StringWriter sw = new StringWriter();
            serializer.transform(new DOMSource(node), new StreamResult(sw));
            return sw.toString();
        } catch (final TransformerException e) {
            LOG.error(e.getMessage(), e);
            return fallback;
        } catch (final Exception e) {
            LOG.error(e.getMessage(), e);
            return fallback;
        }
    }

    /**
     * Pretty prints specified HTML content.
     * 
     * @param htmlContent The HTML content
     * @return Pretty printed HTML content
     */
    public static String prettyPrint(final String htmlContent) {
        return ServerServiceRegistry.getInstance().getService(HTMLService.class).prettyPrint(htmlContent);
    }

    /**
     * Replaces all HTML entities occurring in specified HTML content.
     * 
     * @param content The content
     * @return The content with HTML entities replaced
     */
    public static String replaceHTMLEntities(final String content) {
        return ServerServiceRegistry.getInstance().getService(HTMLService.class).replaceHTMLEntities(content);
    }

    /**
     * Maps specified HTML entity - e.g. <code>&amp;uuml;</code> - to corresponding ASCII character.
     * 
     * @param entity The HTML entity
     * @return The corresponding ASCII character or <code>null</code>
     */
    public static Character getHTMLEntity(final String entity) {
        return ServerServiceRegistry.getInstance().getService(HTMLService.class).getHTMLEntity(entity);
    }

    /**
     * Formats plain text to HTML by escaping HTML special characters e.g. <code>&quot;&lt;&quot;</code> is converted to
     * <code>&quot;&amp;lt;&quot;</code>.
     * 
     * @param plainText The plain text
     * @param withQuote Whether to escape quotes (<code>&quot;</code>) or not
     * @return properly escaped HTML content
     */
    public static String htmlFormat(final String plainText, final boolean withQuote) {
        return ServerServiceRegistry.getInstance().getService(HTMLService.class).htmlFormat(plainText, withQuote);
    }

    /**
     * Formats plain text to HTML by escaping HTML special characters e.g. <code>&quot;&lt;&quot;</code> is converted to
     * <code>&quot;&amp;lt;&quot;</code>.
     * <p>
     * This is just a convenience method which invokes <code>{@link #htmlFormat(String, boolean)}</code> with latter parameter set to
     * <code>true</code>.
     * 
     * @param plainText The plain text
     * @return properly escaped HTML content
     * @see #htmlFormat(String, boolean)
     */
    public static String htmlFormat(final String plainText) {
        return ServerServiceRegistry.getInstance().getService(HTMLService.class).htmlFormat(plainText);
    }

    private static final String DEFAULT_COLOR = "#0026ff";

    private static final String BLOCKQUOTE_START_TEMPLATE = "<blockquote type=\"cite\" style=\"margin-left: 0px; margin-right: 0px;" + " padding-left: 10px; color:%s; border-left: solid 1px %s;\">";

    /**
     * Determines the quote color for given <code>quotelevel</code>.
     * 
     * @param quotelevel - the quote level
     * @return The color for given <code>quotelevel</code>
     */
    private static String getLevelColor(final int quotelevel) {
        final String[] colors = MailProperties.getInstance().getQuoteLineColors();
        return (colors != null) && (colors.length > 0) ? (quotelevel >= colors.length ? colors[colors.length - 1] : colors[quotelevel]) : DEFAULT_COLOR;
    }

    private static final String BLOCKQUOTE_END = "</blockquote>\n";

    private static final String STR_HTML_QUOTE = "&gt;";

    private static final String STR_SPLIT_BR = "<br[ \t]*/?>";

    private static final String HTML_BREAK = "<br>";

    /**
     * Turns all simple quotes "&amp;gt; " occurring in specified HTML text to colored "&lt;blockquote&gt;" tags according to configured
     * quote colors.
     * 
     * @param htmlText The HTML text
     * @return The HTML text with simple quotes replaced with block quotes
     */
    private static String replaceHTMLSimpleQuotesForDisplay(final String htmlText) {
        final StringBuilder sb = new StringBuilder(htmlText.length());
        final String[] lines = htmlText.split(STR_SPLIT_BR);
        int levelBefore = 0;
        final int llen = lines.length - 1;
        for (int i = 0; i <= llen; i++) {
            String line = lines[i];
            int currentLevel = 0;
            int offset = 0;
            if ((offset = startsWithQuote(line)) != -1) {
                currentLevel++;
                int pos = -1;
                boolean next = true;
                while (next && ((pos = line.indexOf(STR_HTML_QUOTE, offset)) > -1)) {
                    /*
                     * Continue only if next starting position is equal to offset or if just one whitespace character has been skipped
                     */
                    next = ((offset == pos) || ((pos - offset == 1) && Character.isWhitespace(line.charAt(offset))));
                    if (next) {
                        currentLevel++;
                        offset = (pos + 4);
                    }
                }
            }
            if (offset > 0) {
                try {
                    offset = (offset < line.length()) && Character.isWhitespace(line.charAt(offset)) ? offset + 1 : offset;
                } catch (final StringIndexOutOfBoundsException e) {
                    if (LOG.isTraceEnabled()) {
                        LOG.trace(e.getMessage(), e);
                    }
                }
                line = line.substring(offset);
            }
            if (levelBefore < currentLevel) {
                for (; levelBefore < currentLevel; levelBefore++) {
                    final String color = getLevelColor(levelBefore);
                    sb.append(String.format(BLOCKQUOTE_START_TEMPLATE, color, color));
                }
            } else if (levelBefore > currentLevel) {
                for (; levelBefore > currentLevel; levelBefore--) {
                    sb.append(BLOCKQUOTE_END);
                }
            }
            sb.append(line);
            if (i < llen) {
                sb.append(HTML_BREAK);
            }
        }
        return sb.toString();
    }

    private static final Pattern PAT_STARTS_WITH_QUOTE = Pattern.compile("\\s*&gt;\\s*", Pattern.CASE_INSENSITIVE);

    private static int startsWithQuote(final String str) {
        final Matcher m = PAT_STARTS_WITH_QUOTE.matcher(str);
        if (m.find() && (m.start() == 0)) {
            return m.end();
        }
        return -1;
    }

    private static final Pattern BACKGROUND_PATTERN = Pattern.compile(
        "(<[a-zA-Z]+[^>]*?)(?:(?:background=cid:([^\\s>]*))|(?:background=\"cid:([^\"]*)\"))([^>]*/?>)",
        Pattern.CASE_INSENSITIVE | Pattern.DOTALL);

    private static final Pattern IMG_PATTERN = Pattern.compile("<img[^>]*>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);

    private static final Pattern CID_PATTERN = Pattern.compile(
        "(?:src=cid:([^\\s>]*))|(?:src=\"cid:([^\"]*)\")",
        Pattern.CASE_INSENSITIVE | Pattern.DOTALL);

    private static final Pattern FILENAME_PATTERN = Pattern.compile(
        "src=\"?([0-9a-z&&[^.\\s>\"]]+\\.[0-9a-z&&[^.\\s>\"]]+)\"?",
        Pattern.CASE_INSENSITIVE);

    // private static final String STR_AJAX_MAIL = "\"/ajax/mail?";

    private static final String STR_SRC = "src=";

    // private static final String CHARSET_ISO8859 = "ISO-8859-1";

    /**
     * Filters inline images occurring in HTML content of a message:
     * <ul>
     * <li>Inline images<br>
     * The source of inline images is in the message itself. Thus loading the inline image is redirected to the appropriate message (image)
     * attachment identified through header <code>Content-Id</code>; e.g.: <code>&lt;img
     * src=&quot;cid:[cid-value]&quot; ... /&gt;</code>.</li>
     * </ul>
     * 
     * @param content The HTML content possibly containing images
     * @param session The session
     * @param msgUID The message's unique path in mailbox
     * @return The HTML content with all inline images replaced with valid links
     */
    private static String filterInlineImages(final String content, final Session session, final MailPath msgUID) {
        String ret = filterImgInlineImages(content, session, msgUID);
        ret = filterBackgroundInlineImages(ret, session, msgUID);
        return ret;
    }

    private static String filterBackgroundInlineImages(final String content, final Session session, final MailPath msgUID) {
        String reval = content;
        try {
            final Matcher imgMatcher = BACKGROUND_PATTERN.matcher(reval);
            final MatcherReplacer imgReplacer = new MatcherReplacer(imgMatcher, reval);
            final StringBuilder sb = new StringBuilder(reval.length());
            if (imgMatcher.find()) {
                final StringBuilder linkBuilder = new StringBuilder(256);
                /*
                 * Replace inline images with Content-ID
                 */
                do {
                    /*
                     * Extract Content-ID
                     */
                    String cid = imgMatcher.group(2);
                    if (cid == null) {
                        cid = imgMatcher.group(3);
                    }
                    /*
                     * Compose corresponding image data
                     */
                    final ImageService imageService = ServerServiceRegistry.getInstance().getService(ImageService.class);
                    final String imageURL;
                    if (null == imageService) {
                        LOG.warn("Missing image service.", new Throwable());
                        imageURL = "";
                    } else {
                        final InlineImageDataSource imgSource = new InlineImageDataSource();
                        final DataArguments args = new DataArguments();
                        final String[] argsNames = imgSource.getRequiredArguments();
                        args.put(argsNames[0], prepareFullname(msgUID.getAccountId(), msgUID.getFolder()));
                        args.put(argsNames[1], String.valueOf(msgUID.getMailID()));
                        args.put(argsNames[2], cid);
                        imageURL = imageService.addImageData(session, imgSource, args, 60000).getImageURL();
                    }
                    linkBuilder.setLength(0);
                    linkBuilder.append(imgMatcher.group(1)).append("background=\"").append(imageURL).append('"').append(imgMatcher.group(4));
                    imgReplacer.appendLiteralReplacement(sb, linkBuilder.toString());
                } while (imgMatcher.find());
            }
            imgReplacer.appendTail(sb);
            reval = sb.toString();
        } catch (final Exception e) {
            LOG.warn("Unable to filter cid background images: " + e.getMessage());
        }
        return reval;
    }

    /**
     * Filters inline images occurring in HTML content of a message:
     * <ul>
     * <li>Inline images<br>
     * The source of inline images is in the message itself. Thus loading the inline image is redirected to the appropriate message (image)
     * attachment identified through header <code>Content-Id</code>; e.g.: <code>&lt;img
     * src=&quot;cid:[cid-value]&quot; ... /&gt;</code>.</li>
     * </ul>
     * 
     * @param content The HTML content possibly containing images
     * @param session The session
     * @param msgUID The message's unique path in mailbox
     * @return The HTML content with all inline images replaced with valid links
     */
    private static String filterImgInlineImages(final String content, final Session session, final MailPath msgUID) {
        String reval = content;
        try {
            final Matcher imgMatcher = IMG_PATTERN.matcher(reval);
            final MatcherReplacer imgReplacer = new MatcherReplacer(imgMatcher, reval);
            final StringBuilder sb = new StringBuilder(reval.length());
            if (imgMatcher.find()) {
                final StringBuilder strBuffer = new StringBuilder(256);
                final MatcherReplacer mr = new MatcherReplacer();
                final StringBuilder linkBuilder = new StringBuilder(256);
                /*
                 * Replace inline images with Content-ID
                 */
                do {
                    final String imgTag = imgMatcher.group();
                    if (!(replaceImgSrc(session, msgUID, imgTag, strBuffer, linkBuilder))) {
                        /*
                         * No cid pattern found, try with filename
                         */
                        strBuffer.setLength(0);
                        final Matcher m = FILENAME_PATTERN.matcher(imgTag);
                        mr.resetTo(m, imgTag);
                        if (m.find()) {
                            final String filename = m.group(1);
                            /*
                             * Compose corresponding image data
                             */
                            final ImageService imageService = ServerServiceRegistry.getInstance().getService(ImageService.class);
                            final String imageURL;
                            if (null == imageService) {
                                imageURL = "";
                            } else {
                                final InlineImageDataSource imgSource = new InlineImageDataSource();
                                final DataArguments args = new DataArguments();
                                final String[] argsNames = imgSource.getRequiredArguments();
                                args.put(argsNames[0], prepareFullname(msgUID.getAccountId(), msgUID.getFolder()));
                                args.put(argsNames[1], String.valueOf(msgUID.getMailID()));
                                args.put(argsNames[2], filename);
                                imageURL = imageService.addImageData(session, imgSource, args, 60000).getImageURL();
                            }
                            linkBuilder.setLength(0);
                            linkBuilder.append(STR_SRC).append('"').append(imageURL).append('"');
                            mr.appendLiteralReplacement(strBuffer, linkBuilder.toString());
                        }
                        mr.appendTail(strBuffer);
                    }
                    imgReplacer.appendLiteralReplacement(sb, strBuffer.toString());
                    strBuffer.setLength(0);
                } while (imgMatcher.find());
            }
            imgReplacer.appendTail(sb);
            reval = sb.toString();
        } catch (final Exception e) {
            LOG.warn("Unable to filter cid Images: " + e.getMessage());
        }
        return reval;
    }

    private static boolean replaceImgSrc(final Session session, final MailPath msgUID, final String imgTag, final StringBuilder cidBuffer, final StringBuilder linkBuilder) {
        boolean retval = false;
        final Matcher cidMatcher = CID_PATTERN.matcher(imgTag);
        final MatcherReplacer cidReplacer = new MatcherReplacer(cidMatcher, imgTag);
        if (cidMatcher.find()) {
            retval = true;
            do {
                /*
                 * Extract Content-ID
                 */
                String cid = cidMatcher.group(1);
                if (cid == null) {
                    cid = cidMatcher.group(2);
                }
                /*
                 * Compose corresponding image data
                 */
                final ImageService imageService = ServerServiceRegistry.getInstance().getService(ImageService.class);
                final String imageURL;
                if (null == imageService) {
                    LOG.warn("Missing image service.", new Throwable());
                    imageURL = "";
                } else {
                    final InlineImageDataSource imgSource = new InlineImageDataSource();
                    final DataArguments args = new DataArguments();
                    final String[] argsNames = imgSource.getRequiredArguments();
                    args.put(argsNames[0], prepareFullname(msgUID.getAccountId(), msgUID.getFolder()));
                    args.put(argsNames[1], String.valueOf(msgUID.getMailID()));
                    args.put(argsNames[2], cid);
                    imageURL = imageService.addImageData(session, imgSource, args, 60000).getImageURL();
                }
                linkBuilder.setLength(0);
                linkBuilder.append(STR_SRC).append('"').append(imageURL).append('"');
                cidReplacer.appendLiteralReplacement(cidBuffer, linkBuilder.toString());
            } while (cidMatcher.find());
        }
        cidReplacer.appendTail(cidBuffer);
        return retval;
    }

    /**
     * Translates specified string into application/x-www-form-urlencoded format using a specific encoding scheme. This method uses the
     * supplied encoding scheme to obtain the bytes for unsafe characters.
     * 
     * @param text The string to be translated.
     * @param charset The character encoding to use; should be <code>UTF-8</code> according to W3C
     * @return The translated string or the string itself if any error occurred
     */
    public static String urlEncodeSafe(final String text, final String charset) {
        try {
            return URLEncoder.encode(text, charset);
        } catch (final UnsupportedEncodingException e) {
            LOG.error(e.getMessage(), e);
            return text;
        }
    }

    /**
     * Initializes a new {@link HTMLProcessing}.
     */
    private HTMLProcessing() {
        super();
    }
}
