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 ro...@apache.org on 2019/06/18 08:27:58 UTC
[james-project] 01/14: JAMES-1975 Move JDKIM mailet as a server
mailet
This is an automated email from the ASF dual-hosted git repository.
rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 3c79a9fc019c8ffd5d5167d8daf60f6b22b272d6
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Aug 31 17:39:28 2018 +0700
JAMES-1975 Move JDKIM mailet as a server mailet
We introduce it in a separate maven module.
All code was just copied.
---
server/mailet/dkim/pom.xml | 106 ++++++
.../james/jdkim/mailets/CRLFOutputStream.java | 161 +++++++++
.../apache/james/jdkim/mailets/ConvertTo7Bit.java | 77 +++++
.../org/apache/james/jdkim/mailets/DKIMSign.java | 173 ++++++++++
.../org/apache/james/jdkim/mailets/DKIMVerify.java | 108 ++++++
.../jdkim/mailets/HeaderSkippingOutputStream.java | 71 ++++
.../james/jdkim/mailets/MimeMessageHeaders.java | 70 ++++
.../apache/james/jdkim/mailets/DKIMSignTest.java | 361 +++++++++++++++++++++
.../apache/james/jdkim/mailets/DKIMVerifyTest.java | 127 ++++++++
server/pom.xml | 2 +
10 files changed, 1256 insertions(+)
diff --git a/server/mailet/dkim/pom.xml b/server/mailet/dkim/pom.xml
new file mode 100644
index 0000000..4d4076a
--- /dev/null
+++ b/server/mailet/dkim/pom.xml
@@ -0,0 +1,106 @@
+<?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/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.james</groupId>
+ <artifactId>james-server</artifactId>
+ <version>3.4.0-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>james-server-mailet-dkim</artifactId>
+ <name>Apache James :: Server :: Mailet :: DKIM</name>
+
+ <description>Mailet adapter for the JDKIM library</description>
+
+ <properties>
+ <jdkim.version>0.3-SNAPSHOT</jdkim.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.james</groupId>
+ <artifactId>apache-mailet-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.james</groupId>
+ <artifactId>apache-mailet-base</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.james</groupId>
+ <artifactId>apache-mailet-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.james.jdkim</groupId>
+ <artifactId>apache-jdkim-library</artifactId>
+ <version>${jdkim.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.james</groupId>
+ <artifactId>apache-mime4j-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.james</groupId>
+ <artifactId>apache-mime4j-dom</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.james.jdkim</groupId>
+ <artifactId>apache-jdkim-library</artifactId>
+ <version>${jdkim.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.james</groupId>
+ <artifactId>apache-mime4j-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.james</groupId>
+ <artifactId>apache-mime4j-dom</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>commons-logging</groupId>
+ <artifactId>commons-logging</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>ca.juliusdavies</groupId>
+ <artifactId>not-yet-commons-ssl</artifactId>
+ <version>0.3.11</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <version>1.7</version>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/CRLFOutputStream.java b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/CRLFOutputStream.java
new file mode 100644
index 0000000..b686b72
--- /dev/null
+++ b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/CRLFOutputStream.java
@@ -0,0 +1,161 @@
+/****************************************************************
+ * 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.jdkim.mailets;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A Filter for use with SMTP or other protocols in which lines must end with
+ * CRLF. Converts every "isolated" occourency of \r or \n with \r\n
+ *
+ * RFC 2821 #2.3.7 mandates that line termination is CRLF, and that CR and LF
+ * must not be transmitted except in that pairing. If we get a naked LF, convert
+ * to CRLF.
+ *
+ */
+public class CRLFOutputStream extends FilterOutputStream {
+
+ /**
+ * Counter for number of last (0A or 0D).
+ */
+ protected int statusLast;
+
+ protected final static int LAST_WAS_OTHER = 0;
+
+ protected final static int LAST_WAS_CR = 1;
+
+ protected final static int LAST_WAS_LF = 2;
+
+ protected boolean startOfLine = true;
+
+ /**
+ * Constructor that wraps an OutputStream.
+ *
+ * @param out
+ * the OutputStream to be wrapped
+ */
+ public CRLFOutputStream(OutputStream out) {
+ super(out);
+ statusLast = LAST_WAS_LF; // we already assume a CRLF at beginning
+ // (otherwise TOP would not work correctly
+ // !)
+ }
+
+ /**
+ * Writes a byte to the stream Fixes any naked CR or LF to the RFC 2821
+ * mandated CFLF pairing.
+ *
+ * @param b
+ * the byte to write
+ *
+ * @throws IOException
+ * if an error occurs writing the byte
+ */
+ public void write(int b) throws IOException {
+ switch (b) {
+ case '\r':
+ out.write('\r');
+ out.write('\n');
+ startOfLine = true;
+ statusLast = LAST_WAS_CR;
+ break;
+ case '\n':
+ if (statusLast != LAST_WAS_CR) {
+ out.write('\r');
+ out.write('\n');
+ startOfLine = true;
+ }
+ statusLast = LAST_WAS_LF;
+ break;
+ default:
+ // we're no longer at the start of a line
+ out.write(b);
+ startOfLine = false;
+ statusLast = LAST_WAS_OTHER;
+ break;
+ }
+ }
+
+ /**
+ * Provides an extension point for ExtraDotOutputStream to be able to add
+ * dots at the beginning of new lines.
+ *
+ * @see java.io.FilterOutputStream#write(byte[], int, int)
+ */
+ protected void writeChunk(byte buffer[], int offset, int length)
+ throws IOException {
+ out.write(buffer, offset, length);
+ }
+
+ /**
+ * @see java.io.FilterOutputStream#write(byte[], int, int)
+ */
+ public synchronized void write(byte buffer[], int offset, int length)
+ throws IOException {
+ /* optimized */
+ int lineStart = offset;
+ for (int i = offset; i < length + offset; i++) {
+ switch (buffer[i]) {
+ case '\r':
+ // CR case. Write down the last line
+ // and position the new lineStart at the next char
+ writeChunk(buffer, lineStart, i - lineStart);
+ out.write('\r');
+ out.write('\n');
+ startOfLine = true;
+ lineStart = i + 1;
+ statusLast = LAST_WAS_CR;
+ break;
+ case '\n':
+ if (statusLast != LAST_WAS_CR) {
+ writeChunk(buffer, lineStart, i - lineStart);
+ out.write('\r');
+ out.write('\n');
+ startOfLine = true;
+ }
+ lineStart = i + 1;
+ statusLast = LAST_WAS_LF;
+ break;
+ default:
+ statusLast = LAST_WAS_OTHER;
+ }
+ }
+ if (length + offset > lineStart) {
+ writeChunk(buffer, lineStart, length + offset - lineStart);
+ startOfLine = false;
+ }
+ }
+
+ /**
+ * Ensure that the stream is CRLF terminated.
+ *
+ * @throws IOException
+ * if an error occurs writing the byte
+ */
+ public void checkCRLFTerminator() throws IOException {
+ if (statusLast == LAST_WAS_OTHER) {
+ out.write('\r');
+ out.write('\n');
+ statusLast = LAST_WAS_CR;
+ }
+ }
+}
diff --git a/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/ConvertTo7Bit.java b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/ConvertTo7Bit.java
new file mode 100644
index 0000000..53a4e01
--- /dev/null
+++ b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/ConvertTo7Bit.java
@@ -0,0 +1,77 @@
+/****************************************************************
+ * 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.jdkim.mailets;
+
+import java.io.IOException;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import javax.mail.internet.MimePart;
+
+import org.apache.mailet.Mail;
+import org.apache.mailet.base.GenericMailet;
+
+/**
+ * Make sure the message stream is 7bit. Every 8bit part is encoded to
+ * quoted-printable or base64 and the message is saved.
+ */
+public class ConvertTo7Bit extends GenericMailet {
+
+ public void service(Mail mail) throws MessagingException {
+ MimeMessage message = mail.getMessage();
+ try {
+ convertTo7Bit(message);
+ message.saveChanges();
+ } catch (IOException e) {
+ throw new MessagingException("IOException converting message to 7bit: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Converts a message to 7 bit.
+ */
+ private void convertTo7Bit(MimePart part) throws MessagingException, IOException {
+ if (part.isMimeType("multipart/*")) {
+ MimeMultipart parts = (MimeMultipart) part.getContent();
+ int count = parts.getCount();
+ for (int i = 0; i < count; i++) {
+ convertTo7Bit((MimePart) parts.getBodyPart(i));
+ }
+ } else if ("8bit".equals(part.getEncoding())) {
+ // The content may already be in encoded the form (likely with mail
+ // created from a stream). In that case, just changing the encoding
+ // to quoted-printable will mangle the result when this is
+ // transmitted.
+ // We must first convert the content into its native format, set it
+ // back, and only THEN set the transfer encoding to force the
+ // content to be encoded appropriately.
+
+ // if the part doesn't contain text it will be base64 encoded.
+ String contentTransferEncoding = part.isMimeType("text/*") ? "quoted-printable" : "base64";
+ part.setContent(part.getContent(), part.getContentType());
+ part.setHeader("Content-Transfer-Encoding", contentTransferEncoding);
+ part.addHeader("X-MIME-Autoconverted", "from 8bit to "
+ + contentTransferEncoding + " by "
+ + getMailetContext().getServerInfo());
+ }
+ }
+
+}
diff --git a/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/DKIMSign.java b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/DKIMSign.java
new file mode 100644
index 0000000..57cc2b0
--- /dev/null
+++ b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/DKIMSign.java
@@ -0,0 +1,173 @@
+/****************************************************************
+ * 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.jdkim.mailets;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Enumeration;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.mail.Header;
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+
+import org.apache.commons.ssl.PKCS8Key;
+import org.apache.james.jdkim.DKIMSigner;
+import org.apache.james.jdkim.api.BodyHasher;
+import org.apache.james.jdkim.api.Headers;
+import org.apache.james.jdkim.api.SignatureRecord;
+import org.apache.james.jdkim.exceptions.PermFailException;
+import org.apache.mailet.Mail;
+import org.apache.mailet.base.GenericMailet;
+
+/**
+ * This mailet sign a message using the DKIM protocol
+ * If the privateKey is encoded using a password then you can pass
+ * the password as privateKeyPassword parameter.
+ *
+ * Sample configuration:
+ *
+ * <pre><code>
+ * <mailet match="All" class="DKIMSign">
+ * <signatureTemplate>v=1; s=selector; d=example.com; h=from:to:received:received; a=rsa-sha256; bh=; b=;</signatureTemplate>
+ * <privateKey>
+ * -----BEGIN RSA PRIVATE KEY-----
+ * MIICXAIBAAKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoT
+ * M5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRH
+ * r7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB
+ * AoGBAI8XcwnZi0Sq5N89wF+gFNhnREFo3rsJDaCY8iqHdA5DDlnr3abb/yhipw0I
+ * /1HlgC6fIG2oexXOXFWl+USgqRt1kTt9jXhVFExg8mNko2UelAwFtsl8CRjVcYQO
+ * cedeH/WM/mXjg2wUqqZenBmlKlD6vNb70jFJeVaDJ/7n7j8BAkEA9NkH2D4Zgj/I
+ * OAVYccZYH74+VgO0e7VkUjQk9wtJ2j6cGqJ6Pfj0roVIMUWzoBb8YfErR8l6JnVQ
+ * bfy83gJeiQJBAOHk3ow7JjAn8XuOyZx24KcTaYWKUkAQfRWYDFFOYQF4KV9xLSEt
+ * ycY0kjsdxGKDudWcsATllFzXDCQF6DTNIWECQEA52ePwTjKrVnLTfCLEG4OgHKvl
+ * Zud4amthwDyJWoMEH2ChNB2je1N4JLrABOE+hk+OuoKnKAKEjWd8f3Jg/rkCQHj8
+ * mQmogHqYWikgP/FSZl518jV48Tao3iXbqvU9Mo2T6yzYNCCqIoDLFWseNVnCTZ0Q
+ * b+IfiEf1UeZVV5o4J+ECQDatNnS3V9qYUKjj/krNRD/U0+7eh8S2ylLqD3RlSn9K
+ * tYGRMgAtUXtiOEizBH6bd/orzI9V9sw8yBz+ZqIH25Q=
+ * -----END RSA PRIVATE KEY-----
+ * </privateKey>
+ * </mailet>
+ * </code></pre>
+ *
+ * By default the mailet assume that Javamail will convert LF to CRLF when sending
+ * so will compute the hash using converted newlines. If you don't want this
+ * behaviout then set forceCRLF attribute to false.
+ */
+public class DKIMSign extends GenericMailet {
+
+ private String signatureTemplate;
+ private PrivateKey privateKey;
+ private boolean forceCRLF;
+
+ /**
+ * @return the signatureTemplate
+ */
+ protected String getSignatureTemplate() {
+ return signatureTemplate;
+ }
+
+ /**
+ * @return the privateKey
+ */
+ protected PrivateKey getPrivateKey() {
+ return privateKey;
+ }
+
+ public void init() throws MessagingException {
+ signatureTemplate = getInitParameter("signatureTemplate");
+ String privateKeyString = getInitParameter("privateKey");
+ String privateKeyPassword = getInitParameter("privateKeyPassword", null);
+ forceCRLF = getInitParameter("forceCRLF", true);
+ try {
+ PKCS8Key pkcs8 = new PKCS8Key(new ByteArrayInputStream(
+ privateKeyString.getBytes()),
+ privateKeyPassword != null ? privateKeyPassword
+ .toCharArray() : null);
+ privateKey = pkcs8.getPrivateKey();
+ } catch (NoSuchAlgorithmException e) {
+ throw new MessagingException("Unknown private key algorythm: " + e.getMessage(), e);
+ } catch (InvalidKeySpecException e) {
+ throw new MessagingException("PrivateKey should be in base64 encoded PKCS8 (der) format: " + e.getMessage(), e);
+ } catch (GeneralSecurityException e) {
+ throw new MessagingException("General security exception: " + e.getMessage(), e);
+ }
+ }
+
+ public void service(Mail mail) throws MessagingException {
+ DKIMSigner signer = new DKIMSigner(getSignatureTemplate(), getPrivateKey());
+ SignatureRecord signRecord = signer
+ .newSignatureRecordTemplate(getSignatureTemplate());
+ try {
+ BodyHasher bhj = signer.newBodyHasher(signRecord);
+ MimeMessage message = mail.getMessage();
+ Headers headers = new MimeMessageHeaders(message);
+ try {
+ OutputStream os = new HeaderSkippingOutputStream(bhj.getOutputStream());
+ if (forceCRLF) {
+ os = new CRLFOutputStream(os);
+ }
+ message.writeTo(os);
+ bhj.getOutputStream().close();
+ } catch (IOException e) {
+ throw new MessagingException("Exception calculating bodyhash: "
+ + e.getMessage(), e);
+ }
+ String signatureHeader = signer.sign(headers, bhj);
+ // Unfortunately JavaMail does not give us a method to add headers
+ // on top.
+ // message.addHeaderLine(signatureHeader);
+ prependHeader(message, signatureHeader);
+ } catch (PermFailException e) {
+ throw new MessagingException("PermFail while signing: "
+ + e.getMessage(), e);
+ }
+
+ }
+
+ private void prependHeader(MimeMessage message, String signatureHeader)
+ throws MessagingException {
+ List<String> prevHeader = new LinkedList<String>();
+ // read all the headers
+ for (Enumeration<String> e = message.getAllHeaderLines(); e.hasMoreElements();) {
+ String headerLine = e.nextElement();
+ prevHeader.add(headerLine);
+ }
+ // remove all the headers
+ for (Enumeration<Header> e = message.getAllHeaders(); e.hasMoreElements();) {
+ Header header = e.nextElement();
+ message.removeHeader(header.getName());
+ }
+ // add our header
+ message.addHeaderLine(signatureHeader);
+ // add the remaining headers using "addHeaderLine" that won't alter the
+ // insertion order.
+ for (String header : prevHeader) {
+ message.addHeaderLine(header);
+ }
+ }
+
+}
diff --git a/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/DKIMVerify.java b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/DKIMVerify.java
new file mode 100644
index 0000000..ca72876
--- /dev/null
+++ b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/DKIMVerify.java
@@ -0,0 +1,108 @@
+/****************************************************************
+ * 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.jdkim.mailets;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+
+import org.apache.james.jdkim.DKIMVerifier;
+import org.apache.james.jdkim.api.BodyHasher;
+import org.apache.james.jdkim.api.Headers;
+import org.apache.james.jdkim.api.SignatureRecord;
+import org.apache.james.jdkim.exceptions.FailException;
+import org.apache.mailet.Mail;
+import org.apache.mailet.base.GenericMailet;
+
+/**
+ * This mailet verify a message using the DKIM protocol
+ *
+ * Sample configuration:
+ * <pre><code>
+ * <mailet match="All" class="DKIMVerify">
+ * </mailet>
+ * </code></pre>
+ *
+ * By default the mailet assume that Javamail will use LF instead of CRLF
+ * so it will verify the hash using converted newlines. If you don't want this
+ * behaviout then set forceCRLF attribute to false.
+ */
+public class DKIMVerify extends GenericMailet {
+
+ public static final String DKIM_AUTH_RESULT_ATTRIBUTE = "jDKIM.AUTHRESULT";
+
+ protected DKIMVerifier verifier = null;
+ private boolean forceCRLF;
+
+ @Override
+ public void init() throws MessagingException {
+ verifier = new DKIMVerifier();
+ forceCRLF = getInitParameter("forceCRLF", true);
+ }
+
+ public void service(Mail mail) throws MessagingException {
+ try {
+ MimeMessage message = mail.getMessage();
+ List<SignatureRecord> res = verify(verifier, message, forceCRLF);
+ if (res == null || res.isEmpty()) {
+ // neutral
+ mail.setAttribute(DKIM_AUTH_RESULT_ATTRIBUTE, "neutral (no signatures)");
+ } else {
+ // pass
+ StringBuilder msg = new StringBuilder();
+ msg.append("pass");
+ for (SignatureRecord rec : res) {
+ msg.append(" (");
+ msg.append("identity ");
+ msg.append(rec.getIdentity().toString());
+ msg.append(")");
+ }
+ mail.setAttribute(DKIM_AUTH_RESULT_ATTRIBUTE, msg.toString());
+ }
+ } catch (FailException e) {
+ // fail
+ mail.setAttribute(DKIM_AUTH_RESULT_ATTRIBUTE, "fail ("+(e.getRelatedRecordIdentity() != null ? "identity "+ e.getRelatedRecordIdentity() + ": " : "")+e.getMessage()+")");
+ }
+
+ }
+
+ protected static List<SignatureRecord> verify(DKIMVerifier verifier, MimeMessage message, boolean forceCRLF)
+ throws MessagingException, FailException {
+ Headers headers = new MimeMessageHeaders(message);
+ BodyHasher bh = verifier.newBodyHasher(headers);
+ try {
+ if (bh != null) {
+ OutputStream os = new HeaderSkippingOutputStream(bh
+ .getOutputStream());
+ if (forceCRLF) os = new CRLFOutputStream(os);
+ message.writeTo(os);
+ bh.getOutputStream().close();
+ }
+
+ } catch (IOException e) {
+ throw new MessagingException("Exception calculating bodyhash: "
+ + e.getMessage(), e);
+ }
+ return verifier.verify(bh);
+ }
+}
diff --git a/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/HeaderSkippingOutputStream.java b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/HeaderSkippingOutputStream.java
new file mode 100644
index 0000000..16e7f25
--- /dev/null
+++ b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/HeaderSkippingOutputStream.java
@@ -0,0 +1,71 @@
+/****************************************************************
+ * 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.jdkim.mailets;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Ignore writes until the given sequence is found.
+ */
+public class HeaderSkippingOutputStream extends FilterOutputStream {
+
+ boolean inHeaders = true;
+ final byte[] skipTo = "\r\n\r\n".getBytes();
+ int pos = 0;
+
+ public HeaderSkippingOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ public void write(byte[] b, int off, int len) throws IOException {
+ if (inHeaders) {
+ for (int i = off; i < off + len; i++) {
+ if (b[i] == skipTo[pos]) {
+ pos++;
+ if (pos == skipTo.length) {
+ inHeaders = false;
+ if (len - i - 1 > 0)
+ out.write(b, i + 1, len - i - 1);
+ break;
+ }
+ } else
+ pos = 0;
+ }
+ } else {
+ out.write(b, off, len);
+ }
+ }
+
+ public void write(int b) throws IOException {
+ if (inHeaders) {
+ if (skipTo[pos] == b) {
+ pos++;
+ if (pos == skipTo.length)
+ inHeaders = false;
+ } else
+ pos = 0;
+ } else {
+ out.write(b);
+ }
+ }
+
+}
diff --git a/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/MimeMessageHeaders.java b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/MimeMessageHeaders.java
new file mode 100644
index 0000000..3d2a639
--- /dev/null
+++ b/server/mailet/dkim/src/main/java/org/apache/james/jdkim/mailets/MimeMessageHeaders.java
@@ -0,0 +1,70 @@
+/****************************************************************
+ * 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.jdkim.mailets;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+
+import org.apache.james.jdkim.api.Headers;
+
+/**
+ * An adapter to let DKIMSigner read headers from MimeMessage
+ */
+final class MimeMessageHeaders implements Headers {
+
+ private final Map<String, List<String>> headers;
+ private final List<String> fields;
+
+ public MimeMessageHeaders(MimeMessage message)
+ throws MessagingException {
+ headers = new HashMap<String, List<String>>();
+ fields = new LinkedList<String>();
+ for (Enumeration<String> e = message.getAllHeaderLines(); e
+ .hasMoreElements();) {
+ String head = e.nextElement();
+ int p = head.indexOf(':');
+ if (p <= 0)
+ throw new MessagingException("Bad header line: " + head);
+ String headerName = head.substring(0, p).trim();
+ String headerNameLC = headerName.toLowerCase();
+ fields.add(headerName);
+ List<String> strings = headers.get(headerNameLC);
+ if (strings == null) {
+ strings = new LinkedList<String>();
+ headers.put(headerNameLC, strings);
+ }
+ strings.add(head);
+ }
+ }
+
+ public List<String> getFields() {
+ return fields;
+ }
+
+ public List<String> getFields(String name) {
+ return headers.get(name.toLowerCase());
+ }
+}
\ No newline at end of file
diff --git a/server/mailet/dkim/src/test/java/org/apache/james/jdkim/mailets/DKIMSignTest.java b/server/mailet/dkim/src/test/java/org/apache/james/jdkim/mailets/DKIMSignTest.java
new file mode 100644
index 0000000..83ea4d6
--- /dev/null
+++ b/server/mailet/dkim/src/test/java/org/apache/james/jdkim/mailets/DKIMSignTest.java
@@ -0,0 +1,361 @@
+/****************************************************************
+ * 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.jdkim.mailets;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Properties;
+
+import javax.mail.Address;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMessage.RecipientType;
+
+import org.apache.james.jdkim.DKIMVerifier;
+import org.apache.james.jdkim.MockPublicKeyRecordRetriever;
+import org.apache.james.jdkim.api.SignatureRecord;
+import org.apache.james.jdkim.exceptions.FailException;
+import org.apache.james.jdkim.exceptions.PermFailException;
+import org.apache.mailet.Mail;
+import org.apache.mailet.Mailet;
+import org.apache.mailet.base.test.FakeMail;
+import org.apache.mailet.base.test.FakeMailContext;
+import org.apache.mailet.base.test.FakeMailetConfig;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DKIMSignTest {
+
+ private static final String TESTING_PEM = "-----BEGIN RSA PRIVATE KEY-----\r\n" +
+ "MIICXAIBAAKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoT\r\n" +
+ "M5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRH\r\n" +
+ "r7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB\r\n" +
+ "AoGBAI8XcwnZi0Sq5N89wF+gFNhnREFo3rsJDaCY8iqHdA5DDlnr3abb/yhipw0I\r\n" +
+ "/1HlgC6fIG2oexXOXFWl+USgqRt1kTt9jXhVFExg8mNko2UelAwFtsl8CRjVcYQO\r\n" +
+ "cedeH/WM/mXjg2wUqqZenBmlKlD6vNb70jFJeVaDJ/7n7j8BAkEA9NkH2D4Zgj/I\r\n" +
+ "OAVYccZYH74+VgO0e7VkUjQk9wtJ2j6cGqJ6Pfj0roVIMUWzoBb8YfErR8l6JnVQ\r\n" +
+ "bfy83gJeiQJBAOHk3ow7JjAn8XuOyZx24KcTaYWKUkAQfRWYDFFOYQF4KV9xLSEt\r\n" +
+ "ycY0kjsdxGKDudWcsATllFzXDCQF6DTNIWECQEA52ePwTjKrVnLTfCLEG4OgHKvl\r\n" +
+ "Zud4amthwDyJWoMEH2ChNB2je1N4JLrABOE+hk+OuoKnKAKEjWd8f3Jg/rkCQHj8\r\n" +
+ "mQmogHqYWikgP/FSZl518jV48Tao3iXbqvU9Mo2T6yzYNCCqIoDLFWseNVnCTZ0Q\r\n" +
+ "b+IfiEf1UeZVV5o4J+ECQDatNnS3V9qYUKjj/krNRD/U0+7eh8S2ylLqD3RlSn9K\r\n" +
+ "tYGRMgAtUXtiOEizBH6bd/orzI9V9sw8yBz+ZqIH25Q=\r\n" +
+ "-----END RSA PRIVATE KEY-----\r\n";
+ private static final FakeMailContext FAKE_MAIL_CONTEXT = FakeMailContext.defaultContext();
+
+ @Test
+ public void testDKIMSign() throws MessagingException, IOException,
+ FailException {
+ String message = "Received: by 10.XX.XX.12 with SMTP id dfgskldjfhgkljsdfhgkljdhfg;\r\n\tTue, 06 Oct 2009 07:37:34 -0700 (PDT)\r\nReturn-Path: <bo...@example.com>\r\nReceived: from example.co.uk (example.co.uk [XX.XXX.125.19])\r\n\tby mx.example.com with ESMTP id dgdfgsdfgsd.97.2009.10.06.07.37.32;\r\n\tTue, 06 Oct 2009 07:37:32 -0700 (PDT)\r\nFrom: apache@bago.org\r\nTo: apache@bago.org\r\n\r\nbody\r\nprova\r\n";
+
+ Mailet mailet = new DKIMSign();
+
+ FakeMailetConfig mci = FakeMailetConfig.builder()
+ .mailetName("Test")
+ .mailetContext(FAKE_MAIL_CONTEXT)
+ .setProperty(
+ "signatureTemplate",
+ "v=1; s=selector; d=example.com; h=from:to:received:received; a=rsa-sha256; bh=; b=;")
+ .setProperty("privateKey", TESTING_PEM)
+ .build();
+
+ mailet.init(mci);
+
+ Mail mail = FakeMail.builder()
+ .name("test")
+ .mimeMessage(new MimeMessage(Session
+ .getDefaultInstance(new Properties()),
+ new ByteArrayInputStream(message.getBytes())))
+ .build();
+
+ mailet.service(mail);
+
+ Mailet m7bit = new ConvertTo7Bit();
+ m7bit.init(mci);
+ m7bit.service(mail);
+
+ ByteArrayOutputStream rawMessage = new ByteArrayOutputStream();
+ mail.getMessage().writeTo(rawMessage);
+
+ MockPublicKeyRecordRetriever mockPublicKeyRecordRetriever = new MockPublicKeyRecordRetriever(
+ "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoTM5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRHr7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB;",
+ "selector", "example.com");
+ verify(rawMessage, mockPublicKeyRecordRetriever);
+ }
+
+ private List<SignatureRecord> verify(ByteArrayOutputStream rawMessage,
+ MockPublicKeyRecordRetriever mockPublicKeyRecordRetriever)
+ throws MessagingException, FailException {
+ List<SignatureRecord> signs = DKIMVerify.verify(new DKIMVerifier(mockPublicKeyRecordRetriever), new MimeMessage(Session.getDefaultInstance(new Properties()), new ByteArrayInputStream(rawMessage.toByteArray())), true);
+ assertNotNull(signs);
+ assertEquals(1, signs.size());
+ return signs;
+ }
+
+ @Test
+ public void testDKIMSignFuture() throws MessagingException, IOException,
+ FailException {
+ String message = "Received: by 10.XX.XX.12 with SMTP id dfgskldjfhgkljsdfhgkljdhfg;\r\n\tTue, 06 Oct 2009 07:37:34 -0700 (PDT)\r\nReturn-Path: <bo...@example.com>\r\nReceived: from example.co.uk (example.co.uk [XX.XXX.125.19])\r\n\tby mx.example.com with ESMTP id dgdfgsdfgsd.97.2009.10.06.07.37.32;\r\n\tTue, 06 Oct 2009 07:37:32 -0700 (PDT)\r\nFrom: apache@bago.org\r\nTo: apache@bago.org\r\n\r\nbody\r\nprova\r\n";
+
+ Mailet mailet = new DKIMSign();
+
+ FakeMailetConfig mci = FakeMailetConfig.builder()
+ .mailetName("Test")
+ .mailetContext(FAKE_MAIL_CONTEXT)
+ .setProperty(
+ "signatureTemplate",
+ "v=1; t=" + ((System.currentTimeMillis() / 1000) + 1000) + "; s=selector; d=example.com; h=from:to:received:received; a=rsa-sha256; bh=; b=;")
+ .setProperty("privateKey", TESTING_PEM)
+ .build();
+
+ mailet.init(mci);
+
+ Mail mail = FakeMail.builder()
+ .name("test")
+ .mimeMessage(new MimeMessage(Session
+ .getDefaultInstance(new Properties()),
+ new ByteArrayInputStream(message.getBytes())))
+ .build();
+
+ mailet.service(mail);
+
+ Mailet m7bit = new ConvertTo7Bit();
+ m7bit.init(mci);
+ m7bit.service(mail);
+
+ ByteArrayOutputStream rawMessage = new ByteArrayOutputStream();
+ mail.getMessage().writeTo(rawMessage);
+
+ MockPublicKeyRecordRetriever mockPublicKeyRecordRetriever = new MockPublicKeyRecordRetriever(
+ "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoTM5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRHr7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB;",
+ "selector", "example.com");
+ try {
+ verify(rawMessage, mockPublicKeyRecordRetriever);
+ Assert.fail("Expecting signature to be ignored");
+ } catch (PermFailException e) {
+ // signature ignored, so fail for missing signatures.
+ }
+ }
+
+
+ @Test
+ public void testDKIMSignTime() throws MessagingException, IOException,
+ FailException {
+ String message = "Received: by 10.XX.XX.12 with SMTP id dfgskldjfhgkljsdfhgkljdhfg;\r\n\tTue, 06 Oct 2009 07:37:34 -0700 (PDT)\r\nReturn-Path: <bo...@example.com>\r\nReceived: from example.co.uk (example.co.uk [XX.XXX.125.19])\r\n\tby mx.example.com with ESMTP id dgdfgsdfgsd.97.2009.10.06.07.37.32;\r\n\tTue, 06 Oct 2009 07:37:32 -0700 (PDT)\r\nFrom: apache@bago.org\r\nTo: apache@bago.org\r\n\r\nbody\r\nprova\r\n";
+
+ Mailet mailet = new DKIMSign();
+
+ FakeMailetConfig mci = FakeMailetConfig.builder()
+ .mailetName("Test")
+ .mailetContext(FAKE_MAIL_CONTEXT)
+ .setProperty(
+ "signatureTemplate",
+ "v=1; t=; s=selector; d=example.com; h=from:to:received:received; a=rsa-sha256; bh=; b=;")
+ .setProperty("privateKey", TESTING_PEM)
+ .build();
+
+ mailet.init(mci);
+
+ Mail mail = FakeMail.builder()
+ .name("test")
+ .mimeMessage(new MimeMessage(Session
+ .getDefaultInstance(new Properties()),
+ new ByteArrayInputStream(message.getBytes())))
+ .build();
+
+ mailet.service(mail);
+
+ Mailet m7bit = new ConvertTo7Bit();
+ m7bit.init(mci);
+ m7bit.service(mail);
+
+ ByteArrayOutputStream rawMessage = new ByteArrayOutputStream();
+ mail.getMessage().writeTo(rawMessage);
+
+ MockPublicKeyRecordRetriever mockPublicKeyRecordRetriever = new MockPublicKeyRecordRetriever(
+ "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoTM5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRHr7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB;",
+ "selector", "example.com");
+ verify(rawMessage, mockPublicKeyRecordRetriever);
+
+ List<SignatureRecord> rs = verify(rawMessage, mockPublicKeyRecordRetriever);
+
+ // check we have a valued signatureTimestamp
+ Assert.assertNotNull(rs.get(0).getSignatureTimestamp());
+ long ref = System.currentTimeMillis() / 1000;
+ // Chech that the signature timestamp is in the past 60 seconds.
+ Assert.assertTrue(rs.get(0).getSignatureTimestamp() <= ref);
+ Assert.assertTrue(rs.get(0).getSignatureTimestamp() >= ref - 60);
+ }
+
+ @Test
+ public void testDKIMSignMessageAsText() throws MessagingException,
+ IOException, FailException {
+ MimeMessage mm = new MimeMessage(Session
+ .getDefaultInstance(new Properties()));
+ mm.addFrom(new Address[]{new InternetAddress("io@bago.org")});
+ mm.addRecipient(RecipientType.TO, new InternetAddress("io@bago.org"));
+ mm.setText("An 8bit encoded body with \u20ACuro symbol.", "ISO-8859-15");
+
+ Mailet mailet = new DKIMSign();
+
+ FakeMailetConfig mci = FakeMailetConfig.builder()
+ .mailetName("Test")
+ .mailetContext(FAKE_MAIL_CONTEXT)
+ .setProperty(
+ "signatureTemplate",
+ "v=1; s=selector; d=example.com; h=from:to:received:received; a=rsa-sha256; bh=; b=;")
+ .setProperty("privateKey", TESTING_PEM)
+ .build();
+
+ mailet.init(mci);
+
+ Mail mail = FakeMail.builder()
+ .name("test")
+ .mimeMessage(mm)
+ .build();
+
+ Mailet m7bit = new ConvertTo7Bit();
+ m7bit.init(mci);
+
+ mailet.service(mail);
+
+ m7bit.service(mail);
+
+ ByteArrayOutputStream rawMessage = new ByteArrayOutputStream();
+ mail.getMessage().writeTo(rawMessage);
+
+ MockPublicKeyRecordRetriever mockPublicKeyRecordRetriever = new MockPublicKeyRecordRetriever(
+ "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoTM5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRHr7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB;",
+ "selector", "example.com");
+
+ verify(rawMessage, mockPublicKeyRecordRetriever);
+ }
+
+ @Test
+ public void testDKIMSignMessageAsObjectConvertedTo7Bit()
+ throws MessagingException, IOException, FailException {
+ MimeMessage mm = new MimeMessage(Session
+ .getDefaultInstance(new Properties()));
+ mm.addFrom(new Address[]{new InternetAddress("io@bago.org")});
+ mm.addRecipient(RecipientType.TO, new InternetAddress("io@bago.org"));
+ mm.setContent("An 8bit encoded body with \u20ACuro symbol.",
+ "text/plain; charset=iso-8859-15");
+ mm.setHeader("Content-Transfer-Encoding", "8bit");
+ mm.saveChanges();
+
+ FakeMailContext FakeMailContext = FAKE_MAIL_CONTEXT;
+ FakeMailContext.getServerInfo();
+
+ FakeMailetConfig mci = FakeMailetConfig.builder()
+ .mailetName("Test")
+ .mailetContext(FAKE_MAIL_CONTEXT)
+ .setProperty(
+ "signatureTemplate",
+ "v=1; s=selector; d=example.com; h=from:to:received:received; a=rsa-sha256; bh=; b=;")
+ .setProperty("privateKey", TESTING_PEM)
+ .build();
+
+ Mail mail = FakeMail.builder()
+ .name("test")
+ .mimeMessage(mm)
+ .build();
+
+ Mailet mailet = new DKIMSign();
+ mailet.init(mci);
+
+ Mailet m7bit = new ConvertTo7Bit();
+ m7bit.init(mci);
+ m7bit.service(mail);
+
+ mailet.service(mail);
+
+ m7bit.service(mail);
+
+ ByteArrayOutputStream rawMessage = new ByteArrayOutputStream();
+ mail.getMessage().writeTo(rawMessage);
+
+ MockPublicKeyRecordRetriever mockPublicKeyRecordRetriever = new MockPublicKeyRecordRetriever(
+ "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoTM5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRHr7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB;",
+ "selector", "example.com");
+ verify(rawMessage, mockPublicKeyRecordRetriever);
+ }
+
+ @Test
+ public void testDKIMSignMessageAsObjectNotConverted()
+ throws MessagingException, IOException, FailException {
+ MimeMessage mm = new MimeMessage(Session
+ .getDefaultInstance(new Properties()));
+ mm.addFrom(new Address[]{new InternetAddress("io@bago.org")});
+ mm.addRecipient(RecipientType.TO, new InternetAddress("io@bago.org"));
+ mm.setContent("An 8bit encoded body with \u20ACuro symbol.",
+ "text/plain; charset=iso-8859-15");
+ mm.setHeader("Content-Transfer-Encoding", "8bit");
+ mm.saveChanges();
+
+ FakeMailContext FakeMailContext = FAKE_MAIL_CONTEXT;
+ FakeMailContext.getServerInfo();
+
+ FakeMailetConfig mci = FakeMailetConfig.builder()
+ .mailetName("Test")
+ .mailetContext(FAKE_MAIL_CONTEXT)
+ .setProperty(
+ "signatureTemplate",
+ "v=1; s=selector; d=example.com; h=from:to:received:received; a=rsa-sha256; bh=; b=;")
+ .setProperty("privateKey", TESTING_PEM)
+ .build();
+
+ Mail mail = FakeMail.builder()
+ .name("test")
+ .mimeMessage(mm)
+ .build();
+
+ Mailet mailet = new DKIMSign();
+ mailet.init(mci);
+
+ Mailet m7bit = new ConvertTo7Bit();
+ m7bit.init(mci);
+ // m7bit.service(mail);
+
+ mailet.service(mail);
+
+ m7bit.service(mail);
+
+ ByteArrayOutputStream rawMessage = new ByteArrayOutputStream();
+ mail.getMessage().writeTo(rawMessage);
+
+ MockPublicKeyRecordRetriever mockPublicKeyRecordRetriever = new MockPublicKeyRecordRetriever(
+ "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoTM5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRHr7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB;",
+ "selector", "example.com");
+ try {
+ verify(rawMessage, mockPublicKeyRecordRetriever);
+ Assert.fail("Expected PermFail");
+ } catch (PermFailException e) {
+
+ }
+ }
+
+}
diff --git a/server/mailet/dkim/src/test/java/org/apache/james/jdkim/mailets/DKIMVerifyTest.java b/server/mailet/dkim/src/test/java/org/apache/james/jdkim/mailets/DKIMVerifyTest.java
new file mode 100644
index 0000000..5ee901f
--- /dev/null
+++ b/server/mailet/dkim/src/test/java/org/apache/james/jdkim/mailets/DKIMVerifyTest.java
@@ -0,0 +1,127 @@
+/****************************************************************
+ * 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.jdkim.mailets;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.MimeMessage;
+
+import org.apache.james.jdkim.DKIMVerifier;
+import org.apache.james.jdkim.MockPublicKeyRecordRetriever;
+import org.apache.james.jdkim.exceptions.FailException;
+import org.apache.mailet.Mail;
+import org.apache.mailet.Mailet;
+import org.apache.mailet.base.test.FakeMail;
+import org.apache.mailet.base.test.FakeMailContext;
+import org.apache.mailet.base.test.FakeMailetConfig;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DKIMVerifyTest {
+
+ @Test
+ public void testDKIMVerifyPass() throws MessagingException, IOException,
+ FailException {
+ String message = "DKIM-Signature: v=1; d=example.com; t=1284762805; b=ZFfwSIzTQM7k9syRnl9VfQh0/dr99euvBe1gn/DiTrnEZjxyjzQBD2MMvowVdbHpPMtSjtCtehU9zZ3urXmj5iHKujpEkP92FEKinzElkQ2eT2zoxdg1zByPHsKPX+KjrBespAJcO2k052aOK5kIBFxpQumP4aiW7ZklBKSWMBk=; s=selector; a=rsa-sha256; bh=rHOD7fd9xnNxK7OSl5ellpQVF14NNFbOIizqtUMhnio=; h=from:to:received:received;\r\n"
+ + "Received: by 10.XX.XX.12 with SMTP id dfgskldjfhgkljsdfhgkljdhfg;\r\n\tTue, 06 Oct 2009 07:37:34 -0700 (PDT)\r\nReturn-Path: <bo...@example.com>\r\nReceived: from example.co.uk (example.co.uk [XX.XXX.125.19])\r\n\tby mx.example.com with ESMTP id dgdfgsdfgsd.97.2009.10.06.07.37.32;\r\n\tTue, 06 Oct 2009 07:37:32 -0700 (PDT)\r\nFrom: apache@bago.org\r\nTo: apache@bago.org\r\n\r\nbody\r\nprova\r\n";
+
+ Mail mail = process(message);
+
+ String attr = (String) mail.getAttribute(DKIMVerify.DKIM_AUTH_RESULT_ATTRIBUTE);
+ Assert.assertNotNull(attr);
+ Assert.assertTrue(attr.startsWith("pass"));
+ }
+
+ @Test
+ public void testDKIMVerifyFail() throws MessagingException, IOException,
+ FailException {
+ // altered message body
+ String message = "DKIM-Signature: v=1; d=example.com; t=1284762805; b=ZFfwSIzTQM7k9syRnl9VfQh0/dr99euvBe1gn/DiTrnEZjxyjzQBD2MMvowVdbHpPMtSjtCtehU9zZ3urXmj5iHKujpEkP92FEKinzElkQ2eT2zoxdg1zByPHsKPX+KjrBespAJcO2k052aOK5kIBFxpQumP4aiW7ZklBKSWMBk=; s=selector; a=rsa-sha256; bh=rHOD7fd9xnNxK7OSl5ellpQVF14NNFbOIizqtUMhnio=; h=from:to:received:received;\r\n"
+ + "Received: by 10.XX.XX.12 with SMTP id dfgskldjfhgkljsdfhgkljdhfg;\r\n\tTue, 06 Oct 2009 07:37:34 -0700 (PDT)\r\nReturn-Path: <bo...@example.com>\r\nReceived: from example.co.uk (example.co.uk [XX.XXX.125.19])\r\n\tby mx.example.com with ESMTP id dgdfgsdfgsd.97.2009.10.06.07.37.32;\r\n\tTue, 06 Oct 2009 07:37:32 -0700 (PDT)\r\nFrom: apache@bago.org\r\nTo: apache@bago.org\r\n\r\nbody\r\nprova altered\r\n";
+
+ Mail mail = process(message);
+
+ String attr = (String) mail.getAttribute(DKIMVerify.DKIM_AUTH_RESULT_ATTRIBUTE);
+ Assert.assertNotNull(attr);
+ Assert.assertTrue(attr.startsWith("fail"));
+ }
+
+ @Test
+ public void testDKIMVerifyFailInvalid() throws MessagingException, IOException,
+ FailException {
+ // invalid version v=2
+ String message = "DKIM-Signature: v=2; d=example.com; t=1284762805; b=ZFfwSIzTQM7k9syRnl9VfQh0/dr99euvBe1gn/DiTrnEZjxyjzQBD2MMvowVdbHpPMtSjtCtehU9zZ3urXmj5iHKujpEkP92FEKinzElkQ2eT2zoxdg1zByPHsKPX+KjrBespAJcO2k052aOK5kIBFxpQumP4aiW7ZklBKSWMBk=; s=selector; a=rsa-sha256; bh=rHOD7fd9xnNxK7OSl5ellpQVF14NNFbOIizqtUMhnio=; h=from:to:received:received;\r\n"
+ + "Received: by 10.XX.XX.12 with SMTP id dfgskldjfhgkljsdfhgkljdhfg;\r\n\tTue, 06 Oct 2009 07:37:34 -0700 (PDT)\r\nReturn-Path: <bo...@example.com>\r\nReceived: from example.co.uk (example.co.uk [XX.XXX.125.19])\r\n\tby mx.example.com with ESMTP id dgdfgsdfgsd.97.2009.10.06.07.37.32;\r\n\tTue, 06 Oct 2009 07:37:32 -0700 (PDT)\r\nFrom: apache@bago.org\r\nTo: apache@bago.org\r\n\r\nbody\r\nprova\r\n";
+
+ Mail mail = process(message);
+
+ String attr = (String) mail.getAttribute(DKIMVerify.DKIM_AUTH_RESULT_ATTRIBUTE);
+ Assert.assertNotNull(attr);
+ Assert.assertTrue(attr.startsWith("fail"));
+ }
+
+ @Test
+ public void testDKIMVerifyNeutral() throws MessagingException, IOException,
+ FailException {
+ // no signatures!
+ String message = ""
+ + "Received: by 10.XX.XX.12 with SMTP id dfgskldjfhgkljsdfhgkljdhfg;\r\n\tTue, 06 Oct 2009 07:37:34 -0700 (PDT)\r\nReturn-Path: <bo...@example.com>\r\nReceived: from example.co.uk (example.co.uk [XX.XXX.125.19])\r\n\tby mx.example.com with ESMTP id dgdfgsdfgsd.97.2009.10.06.07.37.32;\r\n\tTue, 06 Oct 2009 07:37:32 -0700 (PDT)\r\nFrom: apache@bago.org\r\nTo: apache@bago.org\r\n\r\nbody\r\nprova altered\r\n";
+
+ Mail mail = process(message);
+
+ String attr = (String) mail.getAttribute(DKIMVerify.DKIM_AUTH_RESULT_ATTRIBUTE);
+ Assert.assertNotNull(attr);
+ Assert.assertTrue(attr.startsWith("neutral"));
+ }
+
+ private Mail process(String message) throws MessagingException {
+ Mailet mailet = new DKIMVerify() {
+
+ @Override
+ public void init() throws MessagingException {
+ verifier = new DKIMVerifier(new MockPublicKeyRecordRetriever(
+ "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoTM5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRHr7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB;",
+ "selector", "example.com"));
+ }
+
+ };
+
+ FakeMailetConfig mci = FakeMailetConfig.builder()
+ .mailetName("Test")
+ .mailetContext(FakeMailContext.defaultContext())
+ .build();
+
+ mailet.init(mci);
+
+ Mail mail = FakeMail.builder()
+ .name("test")
+ .mimeMessage(new MimeMessage(Session
+ .getDefaultInstance(new Properties()),
+ new ByteArrayInputStream(message.getBytes())))
+ .build();
+
+ mailet.service(mail);
+ return mail;
+ }
+}
diff --git a/server/pom.xml b/server/pom.xml
index 07e2ae2..aafbbb4 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -72,6 +72,8 @@
<module>dns-service/dnsservice-test</module>
<module>mailet/integration-testing</module>
+
+ <module>mailet/dkim</module>
<module>mailet/mailetcontainer-api</module>
<module>mailet/mailetcontainer-camel</module>
<module>mailet/mailets</module>
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org