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>