blob: ad52255c488da3be389ac291b2e0dc7033a64315 [file] [log] [blame]
/****************************************************************
* 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.mime4j.utils.search;
import com.google.common.collect.ImmutableList;
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;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.CharBuffer;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.List;
/**
* Searches an email for content.
*/
public class MessageMatcher {
public static class MessageMatcherBuilder {
private List<CharSequence> searchContents;
private List<String> contentTypes;
private boolean isCaseInsensitive;
private boolean includeHeaders;
private boolean ignoringMime;
private Logger logger;
public MessageMatcherBuilder() {
this.searchContents = ImmutableList.of();
this.contentTypes = ImmutableList.of();
this.isCaseInsensitive = false;
this.includeHeaders = false;
this.ignoringMime = false;
this.logger = LoggerFactory.getLogger(MessageMatcher.class);
}
public MessageMatcherBuilder searchContents(List<CharSequence> searchContents) {
this.searchContents = searchContents;
return this;
}
public MessageMatcherBuilder contentTypes(List<String> contentTypes) {
this.contentTypes = contentTypes;
return this;
}
public MessageMatcherBuilder caseInsensitive(boolean isCaseInsensitive) {
this.isCaseInsensitive = isCaseInsensitive;
return this;
}
public MessageMatcherBuilder includeHeaders(boolean includeHeaders) {
this.includeHeaders = includeHeaders;
return this;
}
public MessageMatcherBuilder logger(Logger logger) {
this.logger = logger;
return this;
}
public MessageMatcherBuilder ignoringMime(boolean ignoringMime) {
this.ignoringMime = ignoringMime;
return this;
}
public MessageMatcher build() {
return new MessageMatcher(searchContents, isCaseInsensitive, includeHeaders, ignoringMime, contentTypes, logger);
}
}
public static MessageMatcherBuilder builder() {
return new MessageMatcherBuilder();
}
private final Logger logger;
private final List<CharSequence> searchContents;
private final List<String> contentTypes;
private final boolean isCaseInsensitive;
private final boolean includeHeaders;
private final boolean ignoringMime;
private MessageMatcher(List<CharSequence> searchContents, boolean isCaseInsensitive, boolean includeHeaders,
boolean ignoringMime, List<String> contentTypes, Logger logger) {
this.contentTypes = ImmutableList.copyOf(contentTypes);
this.searchContents = ImmutableList.copyOf(searchContents);
this.isCaseInsensitive = isCaseInsensitive;
this.includeHeaders = includeHeaders;
this.ignoringMime = ignoringMime;
this.logger = logger;
}
/**
* Is searchContents 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. It takes the mime structure into account.
* @throws IOException
* @throws MimeException
*/
public boolean messageMatches(final InputStream input) throws IOException, MimeException {
for (CharSequence charSequence : searchContents) {
if (charSequence != null) {
final CharBuffer buffer = createBuffer(charSequence);
if (ignoringMime) {
if (! isFoundIn(new InputStreamReader(input), buffer)) {
return false;
}
} else {
if (!matchBufferInMailBeingMimeAware(input, buffer)) {
return false;
}
}
}
}
return true;
}
private boolean matchBufferInMailBeingMimeAware(final InputStream input, final CharBuffer buffer) throws IOException, MimeException {
try {
MimeConfig config = MimeConfig.custom().setMaxLineLen(-1).setMaxHeaderLen(-1).build();
MimeTokenStream parser = new MimeTokenStream(config);
parser.parse(input);
while (parser.next() != EntityState.T_END_OF_STREAM) {
final EntityState state = parser.getState();
switch (state) {
case T_PREAMBLE:
case T_EPILOGUE:
case T_BODY:
if (contentTypes.isEmpty() || contentTypes.contains(parser.getBodyDescriptor().getMimeType())) {
if (checkBody(buffer, parser)) {
return true;
}
}
break;
case T_FIELD:
if (includeHeaders) {
if (checkHeader(buffer, parser)) {
return true;
}
}
break;
case T_END_BODYPART:
case T_END_HEADER:
case T_END_MESSAGE:
case T_END_MULTIPART:
case T_END_OF_STREAM:
case T_RAW_ENTITY:
case T_START_BODYPART:
case T_START_HEADER:
case T_START_MESSAGE:
case T_START_MULTIPART:
break;
}
}
} catch (IllegalCharsetNameException e) {
handle(e);
} catch (UnsupportedCharsetException e) {
handle(e);
} catch (IllegalStateException e) {
handle(e);
}
return false;
}
private boolean checkHeader(final CharBuffer buffer, MimeTokenStream parser) throws IOException {
final String value = parser.getField().getBody();
final StringReader reader = new StringReader(value);
return isFoundIn(reader, buffer);
}
private boolean checkBody(final CharBuffer buffer, MimeTokenStream parser) throws IOException {
final Reader reader = parser.getReader();
return isFoundIn(reader, buffer);
}
private CharBuffer createBuffer(final CharSequence searchContent) {
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 {
logger.warn("Cannot read MIME body.");
logger.debug("Failed to read body.", e);
}
public boolean isFoundIn(final Reader reader, final CharBuffer buffer) throws IOException {
int read;
while ((read = reader.read()) != -1) {
if (matches(buffer, computeNextChar(isCaseInsensitive, (char) read))) {
return true;
}
}
return false;
}
private char computeNextChar(boolean isCaseInsensitive, char read) {
if (isCaseInsensitive) {
return Character.toUpperCase(read);
} else {
return read;
}
}
private boolean matches(final CharBuffer buffer, final char next) {
if (buffer.hasRemaining()) {
final boolean partialMatch = (buffer.position() > 0);
final char matching = buffer.get();
if (next != matching) {
buffer.rewind();
if (partialMatch) {
return matches(buffer, next);
}
}
} else {
return true;
}
return false;
}
}