Project: james-mailbox
/****************************************************************
 * 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.james.mailbox.store.search; 
 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.Reader; 
import java.io.StringReader; 
import java.nio.CharBuffer; 
import java.nio.charset.IllegalCharsetNameException; 
import java.nio.charset.UnsupportedCharsetException; 
 
import org.apache.james.mime4j.MimeException; 
import org.apache.james.mime4j.stream.EntityState; 
import org.apache.james.mime4j.stream.MimeConfig; 
import org.apache.james.mime4j.stream.MimeTokenStream; 
 
import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
 
/**
 * Searches an email for content. This class should be safe for use by 
 * concurrent threads. 
 */
 
public class MessageSearcher { 
 
    private Logger logger; 
 
    private CharSequence searchContent = null
 
    private boolean isCaseInsensitive = false
 
    private boolean includeHeaders = false
 
    public MessageSearcher() { 
    } 
 
    public MessageSearcher(CharSequence searchContent, 
            boolean isCaseInsensitive, boolean includeHeaders) { 
        super(); 
        this.searchContent = searchContent; 
        this.isCaseInsensitive = isCaseInsensitive; 
        this.includeHeaders = includeHeaders; 
    } 
 
    /**
     * Is the search to include headers? 
     *  
     * @return true if header values are included, false otherwise 
     */
 
    public boolean isIncludeHeaders() { 
        return includeHeaders; 
    } 
 
    /**
     * Sets whether the search should include headers. 
     *  
     * @param includesHeaders 
     *            <code>true</code> if header values are included, <code>false</code> otherwise 
     */
 
    public synchronized void setIncludeHeaders(boolean includesHeaders) { 
        this.includeHeaders = includesHeaders; 
    } 
 
    /**
     * Is this search case insensitive? 
     *  
     * @return true if the search should be case insensitive, false otherwise 
     */
 
    public boolean isCaseInsensitive() { 
        return isCaseInsensitive; 
    } 
 
    /**
     * Sets whether the search should be case insensitive. 
     *  
     * @param isCaseInsensitive 
     *            true for case insensitive searches, false otherwise 
     */
 
    public synchronized void setCaseInsensitive(boolean isCaseInsensitive) { 
        this.isCaseInsensitive = isCaseInsensitive; 
    } 
 
    /**
     * Gets the content to be searched for. 
     *  
     * @return search content, initially null 
     */
 
    public synchronized CharSequence getSearchContent() { 
        return searchContent; 
    } 
 
    /**
     * Sets the content sought. 
     *  
     * @param searchContent 
     *            content sought 
     */
 
    public synchronized void setSearchContent(CharSequence searchContent) { 
        this.searchContent = searchContent; 
    } 
 
    /**
     * Is {@link #getSearchContent()} found in the given input? 
     *  
     * @param input 
     *            <code>InputStream</code> containing an email 
     * @return true if the content exists and the stream contains the content, 
     *         false otherwise 
     * @throws IOException 
     * @throws MimeException 
     */
 
    public boolean isFoundIn(final InputStream input) throws IOException, 
            MimeException { 
        final boolean includeHeaders; 
        final CharSequence searchContent; 
        final boolean isCaseInsensitive; 
        synchronized (this) { 
            includeHeaders = this.includeHeaders; 
            searchContent = this.searchContent; 
            isCaseInsensitive = this.isCaseInsensitive; 
        } 
        final boolean result; 
        if (searchContent == null || "".equals(searchContent)) { 
            final Logger logger = getLogger(); 
            logger.debug("Nothing to search for. "); 
            result = false
        } else { 
            final CharBuffer buffer = createBuffer(searchContent, 
                    isCaseInsensitive); 
            result = parse(input, isCaseInsensitive, includeHeaders, buffer); 
        } 
        return result; 
    } 
 
    private boolean parse(final InputStream input, 
            final boolean isCaseInsensitive, final boolean includeHeaders, 
            final CharBuffer buffer) throws IOException, MimeException { 
        try { 
            boolean result = false
            MimeConfig config = new MimeConfig(); 
            config.setMaxLineLen(-1); 
            config.setMaxHeaderLen(-1); 
 
            MimeTokenStream parser = new MimeTokenStream(config);            parser.parse(input); 
            while (!result && parser.next() != EntityState.T_END_OF_STREAM) { 
                final EntityState state = parser.getState(); 
                switch (state) { 
                    case T_BODY: 
                    case T_PREAMBLE: 
                    case T_EPILOGUE: 
                        result = checkBody(isCaseInsensitive, buffer, result, 
                                parser); 
                        break
                    case T_FIELD: 
                        if (includeHeaders) { 
                            result = checkHeader(isCaseInsensitive, buffer, 
                                    result, parser); 
                        } 
                        break
                } 
            } 
            return result; 
        } catch (IllegalCharsetNameException e) { 
            handle(e); 
        } catch (UnsupportedCharsetException e) { 
            handle(e); 
        } catch (IllegalStateException e) { 
            handle(e); 
        } 
        return false
    } 
 
    private boolean checkHeader(final boolean isCaseInsensitive, 
            final CharBuffer buffer, boolean result, MimeTokenStream parser) 
            throws IOException { 
        final String value = parser.getField().getBody(); 
        final StringReader reader = new StringReader(value); 
        if (isFoundIn(reader, buffer, isCaseInsensitive)) { 
            result = true
        } 
        return result; 
    } 
 
    private boolean checkBody(final boolean isCaseInsensitive, 
            final CharBuffer buffer, boolean result, MimeTokenStream parser) 
            throws IOException { 
        final Reader reader = parser.getReader(); 
        if (isFoundIn(reader, buffer, isCaseInsensitive)) { 
            result = true
        } 
        return result; 
    } 
 
    private CharBuffer createBuffer(final CharSequence searchContent, 
            final boolean isCaseInsensitive) { 
        final CharBuffer buffer; 
        if (isCaseInsensitive) { 
            final int length = searchContent.length(); 
            buffer = CharBuffer.allocate(length); 
            for (int i = 0; i < length; i++) { 
                final char next = searchContent.charAt(i); 
                final char upperCase = Character.toUpperCase(next); 
                buffer.put(upperCase); 
            } 
            buffer.flip(); 
        } else { 
            buffer = CharBuffer.wrap(searchContent); 
        } 
        return buffer; 
    } 
 
    protected void handle(Exception e) throws IOException, MimeException { 
        final Logger logger = getLogger(); 
        logger.warn("Cannot read MIME body."); 
        logger.debug("Failed to read body.", e); 
    } 
 
    private boolean isFoundIn(final Reader reader, final CharBuffer buffer, 
            final boolean isCaseInsensitive) throws IOException { 
        boolean result = false
        int read; 
        while (!result && (read = reader.read()) != -1) { 
            final char next; 
            if (isCaseInsensitive) { 
                next = Character.toUpperCase((char) read); 
            } else { 
                next = (char) read; 
            } 
            result = matches(buffer, next); 
        } 
        return result; 
    } 
 
    private boolean matches(final CharBuffer buffer, final char next) { 
        boolean result = false
        if (buffer.hasRemaining()) { 
            final boolean partialMatch = (buffer.position() > 0); 
            final char matching = buffer.get(); 
            if (next != matching) { 
                buffer.rewind(); 
                if (partialMatch) { 
                    result = matches(buffer, next); 
                } 
            } 
        } else { 
            result = true
        } 
        return result; 
    } 
 
    public final Logger getLogger() { 
        if (logger == null) { 
            logger = LoggerFactory.getLogger(MessageSearcher.class); 
        } 
        return logger; 
    } 
 
    public final void setLogger(Logger logger) { 
        this.logger = logger; 
    } 
}