JSIEVE-73 Factorize code for message body matching
git-svn-id: https://svn.apache.org/repos/asf/james/mime4j/trunk@1722970 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/james-utils/pom.xml b/james-utils/pom.xml
new file mode 100644
index 0000000..e5e90a0
--- /dev/null
+++ b/james-utils/pom.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.james</groupId>
+ <artifactId>apache-mime4j-project</artifactId>
+ <version>0.8.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>apache-mime4j-james-utils</artifactId>
+
+ <packaging>bundle</packaging>
+
+ <name>Apache James :: Mime4j :: James utils</name>
+ <description>Mime tools to be shared across James implementation. It is separated from other mime4J projects as it depends on James guava version.</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.james</groupId>
+ <artifactId>apache-mime4j-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.james</groupId>
+ <artifactId>apache-mime4j-dom</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>16.0</version>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ <version>1.7.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>1.7.7</version>
+ </dependency>
+ </dependencies>
+
+
+</project>
\ No newline at end of file
diff --git a/james-utils/src/main/java/org/apache/james/mime4j/utils/search/MessageMatcher.java b/james-utils/src/main/java/org/apache/james/mime4j/utils/search/MessageMatcher.java
new file mode 100644
index 0000000..ad52255
--- /dev/null
+++ b/james-utils/src/main/java/org/apache/james/mime4j/utils/search/MessageMatcher.java
@@ -0,0 +1,262 @@
+/****************************************************************
+ * 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;
+ }
+
+}
diff --git a/james-utils/src/test/java/org.apache.james.mime4j.utils.search/MessageMatcherTest.java b/james-utils/src/test/java/org.apache.james.mime4j.utils.search/MessageMatcherTest.java
new file mode 100644
index 0000000..56e23df
--- /dev/null
+++ b/james-utils/src/test/java/org.apache.james.mime4j.utils.search/MessageMatcherTest.java
@@ -0,0 +1,163 @@
+/****************************************************************
+ * 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.Lists;
+import com.sun.org.apache.bcel.internal.util.ClassLoader;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MessageMatcherTest {
+
+ @Test
+ public void isFoundInShouldBeAbleToLocateTextFragments() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList("as attachment !"))
+ .caseInsensitive(true)
+ .includeHeaders(false)
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isTrue();
+ }
+
+ @Test
+ public void isFoundInShouldReturnFalseWhenTextIsAbsent() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList("Not in the mail"))
+ .caseInsensitive(true)
+ .includeHeaders(false)
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isFalse();
+ }
+
+ @Test
+ public void isFoundInShouldReturnFalseWhenSearchingHeaderTextOutsideHeaders() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList("message/rfc822"))
+ .caseInsensitive(true)
+ .includeHeaders(false)
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isFalse();
+ }
+
+ @Test
+ public void isFoundInShouldReturnFalseWhenSearchingTextLocatedInOtherMimeParts() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList("as attachment !"))
+ .caseInsensitive(true)
+ .includeHeaders(false)
+ .contentTypes(Lists.newArrayList("invalid"))
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isFalse();
+ }
+
+ @Test
+ public void isFoundInShouldReturnTrueWhenSearchingTextLocatedInSpecifiedMimePart() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList("as attachment !"))
+ .caseInsensitive(true)
+ .includeHeaders(false)
+ .contentTypes(Lists.newArrayList("text/plain"))
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isTrue();
+ }
+
+ @Test
+ public void isFoundInShouldBeAbleToRecognizedMimeTypes() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList(""))
+ .caseInsensitive(true)
+ .includeHeaders(false)
+ .contentTypes(Lists.newArrayList("text/plain"))
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isTrue();
+ }
+
+ @Test
+ public void isFoundInShouldNotBeAffectedByInvalidMimeTypes() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList("as attachment !"))
+ .caseInsensitive(true)
+ .includeHeaders(false)
+ .contentTypes(Lists.newArrayList("text/plain", "invalid"))
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isTrue();
+ }
+
+ @Test
+ public void caseSensitivenessShouldBeTakenIntoAccountWhenTurnedOn() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList("as aTtAchment !"))
+ .caseInsensitive(true)
+ .includeHeaders(false)
+ .contentTypes(Lists.newArrayList("text/plain", "invalid"))
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isTrue();
+ }
+
+ @Test
+ public void caseSensitivenessShouldBeIgnoredWhenTurnedOff() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList("as aTtAchment !"))
+ .caseInsensitive(false)
+ .includeHeaders(false)
+ .contentTypes(Lists.newArrayList("text/plain", "invalid"))
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isFalse();
+ }
+
+ @Test
+ public void headerShouldBeMatchedWhenHeaderMatchingIsTurnedOn() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList("message/rfc822"))
+ .caseInsensitive(true)
+ .includeHeaders(true)
+ .contentTypes(Lists.<String>newArrayList())
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isTrue();
+ }
+
+ @Test
+ public void headerShouldBeMatchedWhenIgnoringMime() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList("message/rfc822"))
+ .ignoringMime(true)
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isTrue();
+ }
+
+ @Test
+ public void isFoundInIgnoringMimeShouldIgnoreMimeStructure() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList("ail signature )\n\n--------------0004"))
+ .ignoringMime(true)
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isTrue();
+ }
+
+ @Test
+ public void isFoundInIgnoringMimeShouldReturnFalseOnNonContainedText() throws Exception {
+ MessageMatcher messageMatcher = MessageMatcher.builder()
+ .searchContents(Lists.<CharSequence>newArrayList("invalid"))
+ .ignoringMime(true)
+ .build();
+ assertThat(messageMatcher.messageMatches(ClassLoader.getSystemResourceAsStream("sampleMail.msg"))).isFalse();
+ }
+
+}
diff --git a/james-utils/src/test/resources/sampleMail.msg b/james-utils/src/test/resources/sampleMail.msg
new file mode 100644
index 0000000..65dae2b
--- /dev/null
+++ b/james-utils/src/test/resources/sampleMail.msg
@@ -0,0 +1,77 @@
+Return-Path: <mbaechler@linagora.com>
+Received: from alderaan.linagora.com (smtp.linagora.dc1 [172.16.18.53])
+ by imap (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA;
+ Wed, 10 Jun 2015 10:45:29 +0200
+X-Sieve: CMU Sieve 2.2
+Received: from [10.69.2.28] (mne69-10-88-173-78-196.fbx.proxad.net [88.173.78.196])
+ (using TLSv1 with cipher DHE-RSA-AES128-SHA (128/128 bits))
+ (No client certificate requested)
+ by alderaan.linagora.com (Postfix) with ESMTPSA id 7F33E728
+ for <btellier@linagora.com>; Wed, 10 Jun 2015 10:45:28 +0200 (CEST)
+Message-ID: <5577F927.2040409@linagora.com>
+Date: Wed, 10 Jun 2015 10:45:27 +0200
+From: Matthieu Baechler <mbaechler@linagora.com>
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Icedove/31.7.0
+MIME-Version: 1.0
+To: btellier@linagora.com
+Subject: Fwd: Courbe Sprint
+References: <556EA5E0.8010008@linagora.com>
+In-Reply-To: <556EA5E0.8010008@linagora.com>
+X-Forwarded-Message-Id: <556EA5E0.8010008@linagora.com>
+Content-Type: multipart/mixed;
+ boundary="------------080603090509090707040003"
+
+This is a multi-part message in MIME format.
+--------------080603090509090707040003
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 7bit
+
+Forward as attachment !
+
+
+
+--------------080603090509090707040003
+Content-Type: message/rfc822;
+ name="Courbe Sprint.eml"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: attachment;
+ filename="Courbe Sprint.eml"
+
+Return-Path: <aduprat@linagora.com>
+Received: from alderaan.linagora.com (smtp.linagora.dc1 [172.16.18.53])
+ by imap (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA;
+ Wed, 03 Jun 2015 08:59:45 +0200
+X-Sieve: CMU Sieve 2.2
+Received: from [10.69.43.36] (mne69-10-88-173-78-196.fbx.proxad.net [88.173.78.196])
+ (using TLSv1 with cipher DHE-RSA-AES128-SHA (128/128 bits))
+ (No client certificate requested)
+ by alderaan.linagora.com (Postfix) with ESMTPSA id 4FBAF720
+ for <mbaechler@linagora.com>; Wed, 3 Jun 2015 08:59:45 +0200 (CEST)
+Message-ID: <556EA5E0.8010008@linagora.com>
+Date: Wed, 03 Jun 2015 08:59:44 +0200
+From: Antoine DUPRAT <aduprat@linagora.com>
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Thunderbird/31.6.0
+MIME-Version: 1.0
+To: "mbaechler@linagora.com" <mbaechler@linagora.com>
+Subject: Courbe Sprint
+Content-Type: multipart/mixed;
+ boundary="------------000406070706000403020805"
+
+This is a multi-part message in MIME format.
+--------------000406070706000403020805
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 8bit
+
+Textual content of one attachment ( mail signature )
+
+--------------000406070706000403020805
+Content-Type: application/vnd.oasis.opendocument.spreadsheet;
+ name="Sprint-2015-05-18.ods"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="Sprint-2015-05-18.ods"
+
+Spread shit datas !
+--------------000406070706000403020805--
+
+--------------080603090509090707040003--
diff --git a/pom.xml b/pom.xml
index f2cc716..18a9507 100644
--- a/pom.xml
+++ b/pom.xml
@@ -44,6 +44,7 @@
<module>examples</module>
<module>assemble</module>
<module>mbox</module>
+ <module>james-utils</module>
</modules>
<scm>