You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by rd...@apache.org on 2008/03/22 12:12:33 UTC
svn commit: r639977 - in /james/server/trunk/imap-codec-library/src:
main/java/org/apache/james/imapserver/codec/decode/
main/java/org/apache/james/imapserver/codec/decode/base/
test/java/org/apache/james/imapserver/codec/decode/imap4rev1/
Author: rdonkin
Date: Sat Mar 22 04:12:32 2008
New Revision: 639977
URL: http://svn.apache.org/viewvc?rev=639977&view=rev
Log:
Quoted string SEARCH CHARSET implementation
Added:
james/server/trunk/imap-codec-library/src/test/java/org/apache/james/imapserver/codec/decode/imap4rev1/SearchCommandParserQuotedCharsetTest.java
Modified:
james/server/trunk/imap-codec-library/src/main/java/org/apache/james/imapserver/codec/decode/ImapRequestLineReader.java
james/server/trunk/imap-codec-library/src/main/java/org/apache/james/imapserver/codec/decode/base/AbstractImapCommandParser.java
Modified: james/server/trunk/imap-codec-library/src/main/java/org/apache/james/imapserver/codec/decode/ImapRequestLineReader.java
URL: http://svn.apache.org/viewvc/james/server/trunk/imap-codec-library/src/main/java/org/apache/james/imapserver/codec/decode/ImapRequestLineReader.java?rev=639977&r1=639976&r2=639977&view=diff
==============================================================================
--- james/server/trunk/imap-codec-library/src/main/java/org/apache/james/imapserver/codec/decode/ImapRequestLineReader.java (original)
+++ james/server/trunk/imap-codec-library/src/main/java/org/apache/james/imapserver/codec/decode/ImapRequestLineReader.java Sat Mar 22 04:12:32 2008
@@ -73,7 +73,9 @@
/**
* Reads the next character in the current line. This method will continue to return
* the same character until the {@link #consume()} method is called.
- * @return The next character.
+ * @return The next character
+ * TODO: character encoding is variable and cannot be determine at the token level;
+ * this char is not accurate reported; should be an octet
* @throws ProtocolException If the end-of-stream is reached.
*/
public char nextChar() throws ProtocolException
Modified: james/server/trunk/imap-codec-library/src/main/java/org/apache/james/imapserver/codec/decode/base/AbstractImapCommandParser.java
URL: http://svn.apache.org/viewvc/james/server/trunk/imap-codec-library/src/main/java/org/apache/james/imapserver/codec/decode/base/AbstractImapCommandParser.java?rev=639977&r1=639976&r2=639977&view=diff
==============================================================================
--- james/server/trunk/imap-codec-library/src/main/java/org/apache/james/imapserver/codec/decode/base/AbstractImapCommandParser.java (original)
+++ james/server/trunk/imap-codec-library/src/main/java/org/apache/james/imapserver/codec/decode/base/AbstractImapCommandParser.java Sat Mar 22 04:12:32 2008
@@ -20,8 +20,11 @@
package org.apache.james.imapserver.codec.decode.base;
import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
@@ -52,6 +55,8 @@
*/
public abstract class AbstractImapCommandParser extends AbstractLogEnabled implements ImapCommandParser, MessagingImapCommandParser
{
+ private static final int QUOTED_BUFFER_INITIAL_CAPACITY = 64;
+
private static final Charset US_ASCII = Charset.forName("US-ASCII");
private ImapCommand command;
@@ -161,7 +166,7 @@
char next = request.nextWordChar();
switch ( next ) {
case '"':
- return consumeQuoted( request );
+ return consumeQuoted( request, charset );
case '{':
return consumeLiteral( request, charset );
default:
@@ -346,23 +351,27 @@
final byte[] bytes = new byte[size];
request.read( bytes );
final ByteBuffer buffer = ByteBuffer.wrap(bytes);
- try {
-
- final String result = charset.newDecoder()
- .onMalformedInput(CodingErrorAction.REPORT)
- .onUnmappableCharacter(CodingErrorAction.REPORT)
- .decode(buffer).toString();
- return result;
-
- } catch (IllegalStateException e) {
- throw new ProtocolException ("Bad character encoding", e);
- } catch (MalformedInputException e) {
- throw new ProtocolException ("Bad character encoding", e);
- } catch (UnmappableCharacterException e) {
- throw new ProtocolException ("Bad character encoding", e);
- } catch (CharacterCodingException e) {
- throw new ProtocolException ("Bad character encoding", e);
- }
+ return decode(charset, buffer);
+ }
+ }
+
+ private String decode(final Charset charset, final ByteBuffer buffer) throws ProtocolException {
+ try {
+
+ final String result = charset.newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT)
+ .decode(buffer).toString();
+ return result;
+
+ } catch (IllegalStateException e) {
+ throw new ProtocolException ("Bad character encoding", e);
+ } catch (MalformedInputException e) {
+ throw new ProtocolException ("Bad character encoding", e);
+ } catch (UnmappableCharacterException e) {
+ throw new ProtocolException ("Bad character encoding", e);
+ } catch (CharacterCodingException e) {
+ throw new ProtocolException ("Bad character encoding", e);
}
}
@@ -398,31 +407,28 @@
/**
* Reads a quoted string value from the request.
*/
- protected String consumeQuoted( ImapRequestLineReader request )
+ protected String consumeQuoted( ImapRequestLineReader request)
+ throws ProtocolException
+ {
+ return consumeQuoted(request, null);
+ }
+
+ /**
+ * Reads a quoted string value from the request.
+ */
+ protected String consumeQuoted( ImapRequestLineReader request, Charset charset )
throws ProtocolException
{
- // The 1st character must be '"'
- consumeChar(request, '"' );
-
- StringBuffer quoted = new StringBuffer();
- char next = request.nextChar();
- while( next != '"' ) {
- if ( next == '\\' ) {
- request.consume();
- next = request.nextChar();
- if ( ! isQuotedSpecial( next ) ) {
- throw new ProtocolException( "Invalid escaped character in quote: '" +
- next + "'" );
- }
- }
- quoted.append( next );
- request.consume();
- next = request.nextChar();
+ if (charset == null) {
+ return consumeQuoted(request, US_ASCII);
+ } else {
+ // The 1st character must be '"'
+ consumeChar(request, '"' );
+ final QuotedStringDecoder decoder = new QuotedStringDecoder(charset);
+ final String result = decoder.decode(request);
+ consumeChar( request, '"' );
+ return result;
}
-
- consumeChar( request, '"' );
-
- return quoted.toString();
}
/**
@@ -643,4 +649,105 @@
}
}
+ /**
+ * Decodes contents of a quoted string.
+ * Charset aware.
+ * One shot, not thread safe.
+ */
+ private static class QuotedStringDecoder {
+ /** Decoder suitable for charset */
+ private final CharsetDecoder decoder;
+
+ /** byte buffer will be filled then flushed to character buffer */
+ private final ByteBuffer buffer;
+ /** character buffer may be dynamically resized */
+ CharBuffer charBuffer;
+
+ public QuotedStringDecoder(Charset charset) {
+ decoder = charset.newDecoder();
+ buffer = ByteBuffer.allocate(QUOTED_BUFFER_INITIAL_CAPACITY);
+ charBuffer = CharBuffer.allocate(QUOTED_BUFFER_INITIAL_CAPACITY);
+ }
+
+ public String decode(ImapRequestLineReader request) throws ProtocolException {
+ try {
+ decoder.reset();
+ char next = request.nextChar();
+ while( next != '"' ) {
+ // fill up byte buffer before decoding
+ if (!buffer.hasRemaining()) {
+ decodeByteBufferToCharacterBuffer(false);
+ }
+ if ( next == '\\' ) {
+ request.consume();
+ next = request.nextChar();
+ if ( ! isQuotedSpecial( next ) ) {
+ throw new ProtocolException( "Invalid escaped character in quote: '" +
+ next + "'" );
+ }
+ }
+ // TODO: nextChar does not report accurate chars so safe to cast to byte
+ buffer.put( (byte) next );
+ request.consume();
+ next = request.nextChar();
+ }
+ completeDecoding();
+ final String result = charBuffer.toString();
+ return result;
+
+ } catch (IllegalStateException e) {
+ throw new ProtocolException ("Bad character encoding", e);
+ }
+ }
+
+ private void completeDecoding() throws ProtocolException {
+ decodeByteBufferToCharacterBuffer(true);
+ flush();
+ charBuffer.flip();
+ }
+
+ private void flush() throws ProtocolException {
+ final CoderResult coderResult = decoder.flush(charBuffer);
+ if (coderResult.isOverflow()) {
+ upsizeCharBuffer();
+ flush();
+ } else if (coderResult.isError()) {
+ throw new ProtocolException("Bad character encoding");
+ }
+ }
+
+ /**
+ * Decodes contents of the byte buffer to the character buffer.
+ * The character buffer will be replaced by a larger one if required.
+ * @param endOfInput is the input ended
+ */
+ private CoderResult decodeByteBufferToCharacterBuffer(final boolean endOfInput) throws ProtocolException {
+ buffer.flip();
+ return decodeMoreBytesToCharacterBuffer(endOfInput);
+ }
+
+ private CoderResult decodeMoreBytesToCharacterBuffer(final boolean endOfInput) throws ProtocolException {
+ final CoderResult coderResult = decoder.decode(buffer, charBuffer, endOfInput);
+ if (coderResult.isOverflow()) {
+ upsizeCharBuffer();
+ return decodeMoreBytesToCharacterBuffer(endOfInput);
+ } else if (coderResult.isError()) {
+ throw new ProtocolException("Bad character encoding");
+ } else if (coderResult.isUnderflow()) {
+ buffer.clear();
+ }
+ return coderResult;
+ }
+
+ /**
+ * Increases the size of the character buffer.
+ */
+ private void upsizeCharBuffer() {
+ final int oldCapacity = charBuffer.capacity();
+ CharBuffer oldBuffer = charBuffer;
+ charBuffer = CharBuffer.allocate(oldCapacity + QUOTED_BUFFER_INITIAL_CAPACITY);
+ oldBuffer.flip();
+ charBuffer.put(oldBuffer);
+ }
+ }
}
Added: james/server/trunk/imap-codec-library/src/test/java/org/apache/james/imapserver/codec/decode/imap4rev1/SearchCommandParserQuotedCharsetTest.java
URL: http://svn.apache.org/viewvc/james/server/trunk/imap-codec-library/src/test/java/org/apache/james/imapserver/codec/decode/imap4rev1/SearchCommandParserQuotedCharsetTest.java?rev=639977&view=auto
==============================================================================
--- james/server/trunk/imap-codec-library/src/test/java/org/apache/james/imapserver/codec/decode/imap4rev1/SearchCommandParserQuotedCharsetTest.java (added)
+++ james/server/trunk/imap-codec-library/src/test/java/org/apache/james/imapserver/codec/decode/imap4rev1/SearchCommandParserQuotedCharsetTest.java Sat Mar 22 04:12:32 2008
@@ -0,0 +1,215 @@
+/****************************************************************
+ * 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.imapserver.codec.decode.imap4rev1;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.apache.james.api.imap.ImapCommand;
+import org.apache.james.api.imap.ImapMessage;
+import org.apache.james.api.imap.ProtocolException;
+import org.apache.james.api.imap.display.HumanReadableTextKey;
+import org.apache.james.api.imap.imap4rev1.Imap4Rev1CommandFactory;
+import org.apache.james.api.imap.imap4rev1.Imap4Rev1MessageFactory;
+import org.apache.james.api.imap.message.request.SearchKey;
+import org.apache.james.api.imap.message.response.imap4rev1.StatusResponse;
+import org.apache.james.api.imap.message.response.imap4rev1.StatusResponseFactory;
+import org.apache.james.imapserver.codec.decode.ImapRequestLineReader;
+import org.jmock.Mock;
+import org.jmock.MockObjectTestCase;
+
+public class SearchCommandParserQuotedCharsetTest extends MockObjectTestCase {
+
+ private static final Charset UTF8 = Charset.forName("UTF-8");
+ private static final Charset ASCII = Charset.forName("US-ASCII");
+ private static final String TAG = "A1";
+ private static final String ASCII_SEARCH_TERM = "A Search Term";
+ private static final String NON_ASCII_SEARCH_TERM = "\u043A\u0430\u043A \u0414\u0435\u043B\u0430?";
+ private static final String LENGTHY_NON_ASCII_SEARCH_TERM = NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM +
+ NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM +
+ NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM +
+ NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM +
+ NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM +
+ NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM +
+ NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM +
+ NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM +
+ NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM +
+ NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM +
+ NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM + NON_ASCII_SEARCH_TERM;
+ private static final byte[] BYTES_LENGTHY_NON_ASCII_SEARCH_TERM = LENGTHY_NON_ASCII_SEARCH_TERM.getBytes(UTF8);
+ private static final byte[] BYTES_NON_ASCII_SEARCH_TERM = NON_ASCII_SEARCH_TERM.getBytes(UTF8);
+ private static final byte[] BYTES_QUOTED_UTF8_LENGTHY_NON_ASCII_SEARCH_TERM = add(add(" \"".getBytes(ASCII), BYTES_LENGTHY_NON_ASCII_SEARCH_TERM), "\"".getBytes(ASCII));
+ private static final byte[] BYTES_QUOTED_UTF8_NON_ASCII_SEARCH_TERM = add(add(" \"".getBytes(ASCII), BYTES_NON_ASCII_SEARCH_TERM), "\"".getBytes(ASCII));
+ private static final byte[] BYTES_UTF8_NON_ASCII_SEARCH_TERM = add(" {16}\r\n".getBytes(ASCII), BYTES_NON_ASCII_SEARCH_TERM);
+ private static final byte[] CHARSET = "CHARSET UTF-8 ".getBytes(ASCII);
+
+ private static final byte[] add(byte[] one, byte[] two) {
+ byte[] results = new byte[one.length + two.length];
+ System.arraycopy(one, 0, results, 0, one.length);
+ System.arraycopy(two, 0, results, one.length, two.length);
+ return results;
+ }
+
+ SearchCommandParser parser;
+ Mock mockStatusResponseFactory;
+ Mock mockCommandFactory;
+ Mock mockMessageFactory;
+ Mock mockCommand;
+ Mock mockMessage;
+ ImapCommand command;
+ ImapMessage message;
+
+ protected void setUp() throws Exception {
+ super.setUp();
+ parser = new SearchCommandParser();
+ mockCommandFactory = mock(Imap4Rev1CommandFactory.class);
+ mockCommandFactory.expects(once()).method("getSearch");
+ mockMessageFactory = mock(Imap4Rev1MessageFactory.class);
+ mockCommand = mock(ImapCommand.class);
+ command = (ImapCommand) mockCommand.proxy();
+ mockMessage = mock(ImapMessage.class);
+ mockStatusResponseFactory = mock(StatusResponseFactory.class);
+ message = (ImapMessage) mockMessage.proxy();
+ parser.init((Imap4Rev1CommandFactory) mockCommandFactory.proxy());
+ parser.setMessageFactory((Imap4Rev1MessageFactory) mockMessageFactory.proxy());
+ parser.setStatusResponseFactory((StatusResponseFactory) mockStatusResponseFactory.proxy());
+ parser.enableLogging(new MockLogger());
+ }
+
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+
+ public void testShouldDecoderLengthyQuotedCharset() throws Exception {
+ SearchKey key = SearchKey.buildBcc(LENGTHY_NON_ASCII_SEARCH_TERM);
+ ImapRequestLineReader reader = new ImapRequestLineReader(new ByteArrayInputStream(
+ add(
+ add(CHARSET, "BCC".getBytes("US-ASCII")), BYTES_QUOTED_UTF8_LENGTHY_NON_ASCII_SEARCH_TERM)),
+ new ByteArrayOutputStream());
+ final SearchKey searchKey = parser.searchKey(reader, null, true);
+ assertEquals(key, searchKey);
+ }
+
+ public void testShouldDecoderQuotedCharset() throws Exception {
+ SearchKey key = SearchKey.buildBcc(NON_ASCII_SEARCH_TERM);
+ ImapRequestLineReader reader = new ImapRequestLineReader(new ByteArrayInputStream(add(add(CHARSET, "BCC".getBytes("US-ASCII")), BYTES_QUOTED_UTF8_NON_ASCII_SEARCH_TERM)),
+ new ByteArrayOutputStream());
+ final SearchKey searchKey = parser.searchKey(reader, null, true);
+ assertEquals(key, searchKey);
+ }
+
+ public void testBadCharset() throws Exception {
+ Collection charsetNames = new HashSet();
+ for (final Iterator it = Charset.availableCharsets().values().iterator(); it.hasNext();) {
+ final Charset charset = (Charset) it.next();
+ final Set aliases = charset.aliases();
+ charsetNames.addAll(aliases);
+ }
+ mockStatusResponseFactory.expects(once()).method("taggedNo").with(eq(TAG), same(command), eq(HumanReadableTextKey.BAD_CHARSET), eq(StatusResponse.ResponseCode.badCharset(charsetNames)));
+ ImapRequestLineReader reader = new ImapRequestLineReader(new ByteArrayInputStream("CHARSET BOGUS ".getBytes("US-ASCII")),
+ new ByteArrayOutputStream());
+ parser.decode(command, reader, TAG, false);
+ }
+
+
+
+ public void testShouldThrowProtocolExceptionWhenBytesAreNotEncodedByCharset() throws Exception {
+ try {
+ ImapRequestLineReader reader = new ImapRequestLineReader(new ByteArrayInputStream(add("CHARSET US-ASCII BCC ".getBytes("US-ASCII"), BYTES_NON_ASCII_SEARCH_TERM)),
+ new ByteArrayOutputStream());
+ parser.decode(command, reader, TAG, false);
+ fail("A protocol exception should be thrown when charset is incompatible with input");
+ } catch (ProtocolException e) {
+ //expected
+ }
+ }
+
+ public void testBCCShouldConvertCharset() throws Exception {
+ SearchKey key = SearchKey.buildBcc(NON_ASCII_SEARCH_TERM);
+ checkUTF8Valid("BCC".getBytes("US-ASCII"), key);
+ }
+
+ public void testBODYShouldConvertCharset() throws Exception {
+ SearchKey key = SearchKey.buildBody(NON_ASCII_SEARCH_TERM);
+ checkUTF8Valid("BODY".getBytes("US-ASCII"), key);
+ }
+
+ public void testCCShouldConvertCharset() throws Exception {
+ SearchKey key = SearchKey.buildCc(NON_ASCII_SEARCH_TERM);
+ checkUTF8Valid("CC".getBytes("US-ASCII"), key);
+ }
+
+ public void testFROMShouldConvertCharset() throws Exception {
+ SearchKey key = SearchKey.buildFrom(NON_ASCII_SEARCH_TERM);
+ checkUTF8Valid("FROM".getBytes("US-ASCII"), key);
+ }
+
+ public void testHEADERShouldConvertCharset() throws Exception {
+ SearchKey key = SearchKey.buildHeader("whatever", NON_ASCII_SEARCH_TERM);
+ checkUTF8Valid("HEADER whatever".getBytes("US-ASCII"), key);
+ }
+
+ public void testSUBJECTShouldConvertCharset() throws Exception {
+ SearchKey key = SearchKey.buildSubject(NON_ASCII_SEARCH_TERM);
+ checkUTF8Valid("SUBJECT".getBytes("US-ASCII"), key);
+ }
+
+ public void testTEXTShouldConvertCharset() throws Exception {
+ SearchKey key = SearchKey.buildText(NON_ASCII_SEARCH_TERM);
+ checkUTF8Valid("TEXT".getBytes("US-ASCII"), key);
+ }
+
+ public void testTOShouldConvertCharset() throws Exception {
+ SearchKey key = SearchKey.buildTo(NON_ASCII_SEARCH_TERM);
+ checkUTF8Valid("TO".getBytes("US-ASCII"), key);
+ }
+
+ public void testASCIICharset() throws Exception {
+ SearchKey key = SearchKey.buildBcc(ASCII_SEARCH_TERM);
+ checkValid("CHARSET US-ASCII BCC \"" + ASCII_SEARCH_TERM + "\"", key, true, "US-ASCII");
+ }
+
+ public void testSimpleUTF8Charset() throws Exception {
+ SearchKey key = SearchKey.buildBcc(ASCII_SEARCH_TERM);
+ checkValid("CHARSET UTF-8 BCC \"" + ASCII_SEARCH_TERM + "\"", key, true, "US-ASCII");
+ }
+
+ private void checkUTF8Valid(byte[] term, final SearchKey key) throws Exception {
+ ImapRequestLineReader reader = new ImapRequestLineReader(new ByteArrayInputStream(add(add(CHARSET, term), BYTES_UTF8_NON_ASCII_SEARCH_TERM)),
+ new ByteArrayOutputStream());
+ final SearchKey searchKey = parser.searchKey(reader, null, true);
+ assertEquals(key, searchKey);
+ }
+
+ private void checkValid(String input, final SearchKey key, boolean isFirst, String charset) throws Exception {
+ ImapRequestLineReader reader = new ImapRequestLineReader(new ByteArrayInputStream(input.getBytes(charset)),
+ new ByteArrayOutputStream());
+
+ final SearchKey searchKey = parser.searchKey(reader, null, isFirst);
+ assertEquals(key, searchKey);
+ }
+
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org