SWS-139: mail support

This commit is contained in:
Arjen Poutsma
2007-11-13 18:11:46 +00:00
parent 4e804631b1
commit bccd721644
27 changed files with 1556 additions and 520 deletions

View File

@@ -1,108 +0,0 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail;
import javax.mail.FetchProfile;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.search.AndTerm;
import javax.mail.search.FlagTerm;
import javax.mail.search.SearchTerm;
/**
* Default implementation of the {@link MonitoringStrategy}. Polls for new messages using a defined {@link
* #setPollingInterval(int) interval}.
*
* @author Arjen Poutsma
*/
public class DefaultMonitoringStrategy extends AbstractPollingMonitoringStrategy {
private boolean deleteMessages = true;
/**
* Sets whether messages should be marked as {@link Flags.Flag#DELETED DELETED} after they have been read. Default
* is <code>true</code>.
*/
public void setDeleteMessages(boolean deleteMessages) {
this.deleteMessages = deleteMessages;
}
/**
* Polls for new messages in the given folder. Calls {@link #createSearchTerm(Folder)}, and uses that created term
* to search for messages in the given folder. Marks the messages as {@link Flags.Flag#DELETED DELETED} if the
* {@link #setDeleteMessages(boolean) deleteMessages} property is set.
*/
protected final Message[] pollForNewMessages(Folder folder) throws MessagingException {
SearchTerm searchTerm = createSearchTerm(folder);
Message[] messages;
if (searchTerm == null) {
messages = folder.getMessages();
}
else {
messages = folder.search(searchTerm);
}
if (messages.length > 0) {
FetchProfile contentsProfile = new FetchProfile();
contentsProfile.add(FetchProfile.Item.ENVELOPE);
contentsProfile.add(FetchProfile.Item.CONTENT_INFO);
folder.fetch(messages, contentsProfile);
if (deleteMessages) {
for (int i = 0; i < messages.length; i++) {
messages[i].setFlag(Flags.Flag.DELETED, true);
}
}
}
return messages;
}
/**
* Creates the search term that defines the messages to look for. Default implementation returns a term that
* searches for all messages in the folder that are {@link Flags.Flag#RECENT RECENT}, not {@link Flags.Flag#ANSWERED
* ANSWERED}, and not {@link Flags.Flag#DELETED DELETED}.
* <p/>
* Return <code>null</code> if all messages should be returned from {@link #pollForNewMessages(Folder)}.
*/
protected SearchTerm createSearchTerm(Folder folder) {
Flags supportedFlags = folder.getPermanentFlags();
SearchTerm searchTerm = null;
if (supportedFlags.contains(Flags.Flag.RECENT)) {
searchTerm = new FlagTerm(new Flags(Flags.Flag.RECENT), true);
}
if (supportedFlags.contains(Flags.Flag.ANSWERED)) {
FlagTerm answeredTerm = new FlagTerm(new Flags(Flags.Flag.ANSWERED), false);
if (searchTerm == null) {
searchTerm = answeredTerm;
}
else {
searchTerm = new AndTerm(searchTerm, answeredTerm);
}
}
if (supportedFlags.contains(Flags.Flag.DELETED)) {
FlagTerm deletedTerm = new FlagTerm(new Flags(Flags.Flag.DELETED), false);
if (searchTerm == null) {
searchTerm = deletedTerm;
}
else {
searchTerm = new AndTerm(searchTerm, deletedTerm);
}
}
return searchTerm;
}
}

View File

@@ -1,60 +0,0 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import org.springframework.util.Assert;
import org.springframework.ws.transport.support.ParameterizedUri;
/**
* @author Arjen Poutsma
*/
public class MailtoUri extends ParameterizedUri {
public MailtoUri(String uri) {
super(uri);
Assert.isTrue(uri.startsWith(MailTransportConstants.URI_SCHEME), "Invalid uri: " + uri);
try {
InternetAddress.parse(getDestination(), false);
}
catch (AddressException ex) {
throw new IllegalArgumentException(ex);
}
}
public InternetAddress getTo() throws AddressException {
return new InternetAddress(getDestination());
}
public String getSubject() {
return getParameter("subject");
}
public boolean hasSubject() {
return hasParameter("subject");
}
public boolean hasCc() {
return hasParameter("cc");
}
public InternetAddress getCc() throws AddressException {
return new InternetAddress(getParameter("cc"));
}
}

View File

@@ -1,84 +0,0 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.support;
import java.util.Map;
import java.util.StringTokenizer;
import org.springframework.core.CollectionFactory;
import org.springframework.util.Assert;
/** @author Arjen Poutsma */
public class ParameterizedUri {
private final String uri;
private final String scheme;
// keys are string parameter names; values are string parameter values
private final Map parameters = CollectionFactory.createLinkedCaseInsensitiveMapIfPossible(5);
private final String destination;
public ParameterizedUri(String uri) {
Assert.hasLength(uri, "'uri' must not be empty");
this.uri = uri;
int scIdx = uri.indexOf(':');
Assert.isTrue(scIdx != -1, uri + " does contain scheme");
scheme = uri.substring(0, scIdx);
Assert.isTrue(uri.length() > scheme.length(), uri + " does not have a destination");
int paramStart = uri.indexOf('?');
if (paramStart == -1) {
destination = uri.substring(scIdx + 1);
}
else {
destination = uri.substring(scIdx + 1, paramStart);
parseParameters(uri.substring(paramStart + 1));
}
}
private void parseParameters(String parametersString) {
StringTokenizer params = new StringTokenizer(parametersString, "&");
while (params.hasMoreTokens()) {
String param = params.nextToken();
int paramSep = param.indexOf('=');
if (paramSep == -1) {
throw new IllegalArgumentException(param + " is not a valid parameter: it has no '='");
}
String paramName = param.substring(0, paramSep);
String paramValue = param.substring(paramSep + 1);
parameters.put(paramName, paramValue);
}
}
/** Returns the destination of the uri. */
protected String getDestination() {
return destination;
}
public String toString() {
return uri;
}
protected String getParameter(String paramName) {
return (String) parameters.get(paramName);
}
protected boolean hasParameter(String paramName) {
return parameters.containsKey(paramName);
}
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.support;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.ws.transport.WebServiceConnection;
import org.springframework.ws.transport.WebServiceMessageReceiver;
/**
* Base class for server-side transport objects which have a predefined {@link WebServiceMessageReceiver}.
*
* @author Arjen Poutsma
* @see #handleConnection(WebServiceConnection)
* @since 1.1.0
*/
public abstract class SimpleWebServiceMessageReceiverObjectSupport extends WebServiceMessageReceiverObjectSupport
implements InitializingBean {
private WebServiceMessageReceiver messageReceiver;
/**
* Returns the <code>WebServiceMessageReceiver</code> used by this listener.
*/
public WebServiceMessageReceiver getMessageReceiver() {
return messageReceiver;
}
/**
* Sets the <code>WebServiceMessageReceiver</code> used by this listener.
*/
public void setMessageReceiver(WebServiceMessageReceiver messageReceiver) {
this.messageReceiver = messageReceiver;
}
public void afterPropertiesSet() throws Exception {
super.afterPropertiesSet();
Assert.notNull(getMessageReceiver(), "messageReceiver must not be null");
}
protected final void handleConnection(WebServiceConnection connection) throws Exception {
handleConnection(connection, getMessageReceiver());
}
}

View File

@@ -1,34 +0,0 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport;
import javax.xml.transform.Transformer;
import org.springframework.ws.context.MessageContext;
import org.springframework.xml.transform.TransformerObjectSupport;
import org.springframework.util.Assert;
public class SimpleTestingMessageReceiver extends TransformerObjectSupport implements WebServiceMessageReceiver {
public void receive(MessageContext messageContext) throws Exception {
Assert.notNull(messageContext, "MessageContext is null");
logger.info("Received message");
Transformer transformer = createTransformer();
transformer.transform(messageContext.getRequest().getPayloadSource(),
messageContext.getResponse().getPayloadResult());
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail;
import java.io.IOException;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author Arjen Poutsma
*/
public class Driver {
public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml", Driver.class);
context.registerShutdownHook();
System.out.println("Started....");
System.in.read();
}
}

View File

@@ -1,64 +0,0 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPConstants;
import javax.xml.soap.SOAPMessage;
import junit.framework.TestCase;
import org.springframework.ws.soap.SoapMessage;
import org.springframework.ws.soap.saaj.SaajSoapMessage;
import org.springframework.ws.transport.WebServiceConnection;
public class MailMessageSenderIntegrationTest extends TestCase {
private MailMessageSender messageSender;
private MessageFactory messageFactory;
private static final String URI = "mailto:ajwpi21@xs4all.nl?subject=SOAP Test";
// private static final String URI = "mailto:revans@interface21.com?subject=Believe me now?";
private static final String SOAP_ACTION = "http://springframework.org/DoIt";
protected void setUp() throws Exception {
messageSender = new MailMessageSender();
messageSender.setFrom("Arjen Poutsma <ajwp@xs4all.nl>");
messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_1_PROTOCOL);
}
public void testSendAndReceiveQueueNoResponse() throws Exception {
WebServiceConnection connection = null;
try {
connection = messageSender.createConnection(URI);
SOAPMessage saajMessage = messageFactory.createMessage();
saajMessage.getSOAPBody().addBodyElement(new QName("http://springframework.org", "test"));
SoapMessage soapRequest = new SaajSoapMessage(saajMessage);
soapRequest.setSoapAction(SOAP_ACTION);
connection.send(soapRequest);
// SoapMessage response = (SoapMessage) connection.receive(new SaajSoapMessageFactory(messageFactory));
}
finally {
if (connection != null) {
connection.close();
}
}
}
}

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="messagingReceiver" class="org.springframework.ws.transport.mail.MailMessageReceiver">
<property name="messageFactory" ref="messageFactory"/>
<property name="messageReceiver">
<bean class="org.springframework.ws.transport.SimpleTestingMessageReceiver"/>
</property>
<property name="monitoringStrategy">
<bean class="org.springframework.ws.transport.mail.DefaultMonitoringStrategy">
<property name="pollingInterval" value="10000"/>
</bean>
</property>
</bean>
<bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>
<bean id="mbeanExporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="spring-ws:service=messagingContainer">
<ref local="messagingReceiver"/>
</entry>
</map>
</property>
<property name="assembler">
<bean class="org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler">
<property name="interfaceMappings">
<props>
<prop key="spring-ws:service=messagingContainer">org.springframework.context.Lifecycle</prop>
</props>
</property>
</bean>
</property>
</bean>
</beans>

View File

@@ -98,12 +98,16 @@
<dependency>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.ejb</groupId>
<artifactId>ejb</artifactId>
<version>2.1</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<optional>true</optional>
</dependency>
<!-- Other dependencies -->

View File

@@ -0,0 +1,280 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail;
import java.util.Properties;
import javax.mail.Folder;
import javax.mail.FolderClosedException;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.URLName;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.springframework.scheduling.SchedulingAwareRunnable;
import org.springframework.util.Assert;
import org.springframework.ws.WebServiceMessageFactory;
import org.springframework.ws.transport.WebServiceMessageReceiver;
import org.springframework.ws.transport.mail.monitor.MonitoringStrategy;
import org.springframework.ws.transport.mail.monitor.PollingMonitoringStrategy;
import org.springframework.ws.transport.mail.monitor.Pop3PollingMonitoringStrategy;
import org.springframework.ws.transport.mail.support.MailTransportUtils;
import org.springframework.ws.transport.support.AbstractAsyncStandaloneMessageReceiver;
/**
* Server-side component for receiving email messages using JavaMail. Requires a {@link #setTransportUri(String)
* transport} URI, {@link #setStoreUri(String) store} URI, and {@link #setMonitoringStrategy(MonitoringStrategy)
* monitoringStrategy} to be set, in addition to the {@link #setMessageFactory(WebServiceMessageFactory) messageFactory}
* and {@link #setMessageReceiver(WebServiceMessageReceiver) messageReceiver} required by the base class.
* <p/>
* The {@link MonitoringStrategy} is used to detect new incoming email request. If the <code>monitoringStrategy</code>
* is not explicitly set, this receiver will use the {@link Pop3PollingMonitoringStrategy} for POP3 servers, and the
* {@link PollingMonitoringStrategy} for IMAP servers.
*
* @author Arjen Poutsma
* @since 1.1.0
*/
public class MailMessageReceiver extends AbstractAsyncStandaloneMessageReceiver {
private Session session = Session.getInstance(new Properties(), null);
private URLName storeUri;
private URLName transportUri;
private Folder folder;
private Store store;
private InternetAddress from;
private MonitoringStrategy monitoringStrategy;
/** Sets the from address to use when sending reponse messages. */
public void setFrom(String from) throws AddressException {
this.from = new InternetAddress(from);
}
/**
* Set JavaMail properties for the {@link Session}.
* <p/>
* A new {@link Session} will be created with those properties. Use either this method or {@link #setSession}, but
* not both.
* <p/>
* Non-default properties in this instance will override given JavaMail properties.
*/
public void setJavaMailProperties(Properties javaMailProperties) {
session = Session.getInstance(javaMailProperties, null);
}
/**
* Set the JavaMail <code>Session</code>, possibly pulled from JNDI.
* <p/>
* Default is a new <code>Session</code> without defaults, that is completely configured via this instance's
* properties.
* <p/>
* If using a pre-configured <code>Session</code>, non-default properties in this instance will override the
* settings in the <code>Session</code>.
*
* @see #setJavaMailProperties
*/
public void setSession(Session session) {
Assert.notNull(session, "Session must not be null");
this.session = session;
}
/**
* Sets the JavaMail Store URI to be used for retrieving request messages. Typically takes the form of
* <code>[imap|pop3]://user:password@host:port/INBOX</code>. Setting this property is required.
* <p/>
* For example, <code>imap://john:secret@imap.example.com/INBOX</code>
*
* @see Session#getStore(URLName)
*/
public void setStoreUri(String storeUri) {
this.storeUri = new URLName(storeUri);
}
/**
* Sets the JavaMail Transport URI to be used for sending response messages. Typically takes the form of
* <code>smtp://user:password@host:port</code>. Setting this property is required.
* <p/>
* For example, <code>smtp://john:secret@smtp.example.com</code>
*
* @see Session#getTransport(URLName)
*/
public void setTransportUri(String transportUri) {
this.transportUri = new URLName(transportUri);
}
/**
* Sets the monitoring strategy to use for retrieving new requests. Default is the {@link
* PollingMonitoringStrategy}.
*/
public void setMonitoringStrategy(MonitoringStrategy monitoringStrategy) {
this.monitoringStrategy = monitoringStrategy;
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(storeUri, "Property 'storeUri' is required");
Assert.notNull(transportUri, "Property 'transportUri' is required");
if (monitoringStrategy == null) {
String protocol = storeUri.getProtocol();
if ("pop3".equals(protocol)) {
monitoringStrategy = new Pop3PollingMonitoringStrategy();
}
else if ("imap".equals(protocol)) {
monitoringStrategy = new PollingMonitoringStrategy();
}
else {
throw new IllegalArgumentException("Cannot determine monitoring strategy for \"" + protocol + "\". " +
"Set the 'monitoringStrategy' explicitly.");
}
}
super.afterPropertiesSet();
}
protected void onActivate() throws MessagingException {
openSession();
openFolder();
}
protected void onStart() {
if (logger.isInfoEnabled()) {
logger.info("Starting mail receiver [" + MailTransportUtils.toPasswordProtectedString(storeUri) + "]");
}
execute(new MonitoringRunnable());
}
protected void onStop() {
if (logger.isInfoEnabled()) {
logger.info("Stopping mail receiver [" + MailTransportUtils.toPasswordProtectedString(storeUri) + "]");
}
closeFolder();
}
protected void onShutdown() {
if (logger.isInfoEnabled()) {
logger.info("Shutting down mail receiver [" + MailTransportUtils.toPasswordProtectedString(storeUri) + "]");
}
closeFolder();
closeSession();
}
private void openSession() throws MessagingException {
store = session.getStore(storeUri);
if (logger.isDebugEnabled()) {
logger.debug("Connecting to store [" + MailTransportUtils.toPasswordProtectedString(storeUri) + "]");
}
store.connect();
}
private void openFolder() throws MessagingException {
if (folder != null && folder.isOpen()) {
return;
}
folder = store.getFolder(storeUri);
if (folder == null || !folder.exists()) {
throw new IllegalStateException("No default folder to receive from");
}
if (logger.isDebugEnabled()) {
logger.debug("Opening folder [" + MailTransportUtils.toPasswordProtectedString(storeUri) + "]");
}
folder.open(monitoringStrategy.getFolderOpenMode());
}
private void closeFolder() {
MailTransportUtils.closeFolder(folder, true);
}
private void closeSession() {
MailTransportUtils.closeService(store);
}
private class MonitoringRunnable implements SchedulingAwareRunnable {
public void run() {
try {
openFolder();
while (isRunning()) {
try {
Message[] messages = monitoringStrategy.monitor(folder);
for (int i = 0; i < messages.length; i++) {
if (logger.isDebugEnabled()) {
if (messages[i] instanceof MimeMessage) {
MimeMessage mimeMessage = (MimeMessage) messages[i];
logger.debug("Received email message with MessageID " + mimeMessage.getMessageID());
}
}
MessageHandler handler = new MessageHandler(messages[i]);
execute(handler);
}
}
catch (FolderClosedException ex) {
logger.debug("Folder closed, reopening");
if (isRunning()) {
openFolder();
}
}
catch (MessagingException ex) {
logger.warn(ex);
}
}
}
catch (InterruptedException ex) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
catch (MessagingException ex) {
logger.error(ex);
}
}
public boolean isLongLived() {
return true;
}
}
private class MessageHandler implements SchedulingAwareRunnable {
private final Message message;
public MessageHandler(Message message) {
this.message = message;
}
public void run() {
MailReceiverConnection connection = new MailReceiverConnection(message, session);
connection.setTransportUri(transportUri);
connection.setFrom(from);
try {
handleConnection(connection);
}
catch (Exception ex) {
logger.error("Could not handle incoming mail connection", ex);
}
}
public boolean isLongLived() {
return false;
}
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail;
import java.io.IOException;
import java.net.URI;
import java.util.Properties;
import javax.mail.Session;
import javax.mail.URLName;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.ws.WebServiceMessageFactory;
import org.springframework.ws.transport.WebServiceConnection;
import org.springframework.ws.transport.WebServiceMessageSender;
import org.springframework.ws.transport.mail.monitor.PollingMonitoringStrategy;
import org.springframework.ws.transport.mail.support.MailTransportUtils;
/**
* {@link WebServiceMessageSender} implementation that uses Mail {@link MimeMessage}s. Requires a {@link
* #setTransportUri(String) transport} and {@link #setStoreUri(String) store} URI to be set.
* <p/>
* Calling {@link WebServiceConnection#receive(WebServiceMessageFactory)} on connections created by this message sender
* will result in a blocking call, for the amount of milliseconds specified by the {@link #setReceiveSleepTime(long)
* receiveSleepTime} property. This will give the server time to formulate a response message. By default, this propery
* is set to 1 minute. For a proper request-response conversation to work, this property value must not be smaller the
* {@link PollingMonitoringStrategy#setPollingInterval(long) pollingInterval} property of the server-side message
* receiver polling strategy.
* <p/>
* This message sender supports URI's of the following format: <blockquote> <tt><b>mailto:</b></tt><i>to</i>[<tt><b>?</b></tt><i>param-name</i><tt><b>=</b></tt><i>param-value</i>][<tt><b>&amp;</b></tt><i>param-name</i><tt><b>=</b></tt><i>param-value</i>]*
* </blockquote> where the characters <tt><b>:</b></tt>, <tt><b>?</b></tt>, and <tt><b>&amp;</b></tt> stand for
* themselves. The <i>to</i> represents a RFC 822 mailbox. Valid <i>param-name</i> include:
* <p/>
* <blockquote><table> <tr><th><i>param-name</i></th><th><i>Description</i></th></tr>
* <tr><td><tt>subject</tt></td><td>The subject of the request message.</td></tr> </table></blockquote>
* <p/>
* Some examples of email URIs are:
* <p/>
* <blockquote><tt>mailto:john@example.com</tt><br> <tt>mailto:john@example.com@?subject=SOAP%20Test</tt><br></blockquote>
*
* @author Arjen Poutsma
* @see <a href="http://www.ietf.org/rfc/rfc2368.txt">The mailto URL scheme</a>
* @since 1.1.0
*/
public class MailMessageSender implements WebServiceMessageSender, InitializingBean {
/** Default timeout for receive operations. Set to 1000 * 60 milliseconds (i.e. 1 minute). */
public static final long DEFAULT_RECEIVE_TIMEOUT = 1000 * 60;
private long receiveSleepTime = DEFAULT_RECEIVE_TIMEOUT;
private Session session = Session.getInstance(new Properties(), null);
private URLName storeUri;
private URLName transportUri;
private InternetAddress from;
/** Sets the from address to use when sending request messages. */
public void setFrom(String from) throws AddressException {
this.from = new InternetAddress(from);
}
/**
* Set JavaMail properties for the {@link Session}.
* <p/>
* A new {@link Session} will be created with those properties. Use either this method or {@link #setSession}, but
* not both.
* <p/>
* Non-default properties in this instance will override given JavaMail properties.
*/
public void setJavaMailProperties(Properties javaMailProperties) {
session = Session.getInstance(javaMailProperties, null);
}
/**
* Set the sleep time to use for receive calls, <strong>in milliseconds</strong>. The default is 1000 * 60 ms, that
* is 1 minute.
*/
public void setReceiveSleepTime(long receiveSleepTime) {
this.receiveSleepTime = receiveSleepTime;
}
/**
* Set the JavaMail <code>Session</code>, possibly pulled from JNDI.
* <p/>
* Default is a new <code>Session</code> without defaults, that is completely configured via this instance's
* properties.
* <p/>
* If using a pre-configured <code>Session</code>, non-default properties in this instance will override the
* settings in the <code>Session</code>.
*
* @see #setJavaMailProperties
*/
public void setSession(Session session) {
Assert.notNull(session, "Session must not be null");
this.session = session;
}
/**
* Sets the JavaMail Store URI to be used for retrieving response messages. Typically takes the form of
* <code>[imap|pop3]://user:password@host:port/INBOX</code>. Setting this property is required.
* <p/>
* For example, <code>imap://john:secret@imap.example.com/INBOX</code>
*
* @see Session#getStore(URLName)
*/
public void setStoreUri(String storeUri) {
this.storeUri = new URLName(storeUri);
}
/**
* Sets the JavaMail Transport URI to be used for sending response messages. Typically takes the form of
* <code>smtp://user:password@host:port</code>. Setting this property is required.
* <p/>
* For example, <code>smtp://john:secret@smtp.example.com</code>
*
* @see Session#getTransport(URLName)
*/
public void setTransportUri(String transportUri) {
this.transportUri = new URLName(transportUri);
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(transportUri, "'transportUri' is required");
Assert.notNull(storeUri, "'storeUri' is required");
}
public WebServiceConnection createConnection(URI uri) throws IOException {
InternetAddress to = MailTransportUtils.getTo(uri);
MailSenderConnection connection =
new MailSenderConnection(session, transportUri, storeUri, to, receiveSleepTime);
if (from != null) {
connection.setFrom(from);
}
String subject = MailTransportUtils.getSubject(uri);
if (subject != null) {
connection.setSubject(subject);
}
return connection;
}
public boolean supports(URI uri) {
return uri.getScheme().equals(MailTransportConstants.MAIL_URI_SCHEME);
}
}

View File

@@ -40,10 +40,15 @@ import org.springframework.util.Assert;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.transport.AbstractReceiverConnection;
import org.springframework.ws.transport.TransportConstants;
import org.springframework.ws.transport.mail.support.MailUtils;
import org.springframework.ws.transport.WebServiceConnection;
import org.springframework.ws.transport.mail.support.MailTransportUtils;
/**
* Implementation of {@link WebServiceConnection} that is used for server-side Mail access. Exposes a {@link Message}
* request and response message.
*
* @author Arjen Poutsma
* @since 1.1.0
*/
public class MailReceiverConnection extends AbstractReceiverConnection {
@@ -61,13 +66,39 @@ public class MailReceiverConnection extends AbstractReceiverConnection {
private InternetAddress from;
public MailReceiverConnection(Message requestMessage, Session session) {
/** Constructs a new Mail connection with the given parameters. */
protected MailReceiverConnection(Message requestMessage, Session session) {
Assert.notNull(requestMessage, "'requestMessage' must not be null");
Assert.notNull(session, "'session' must not be null");
this.requestMessage = requestMessage;
this.session = session;
}
/** Returns the request message for this connection. */
public Message getRequestMessage() {
return requestMessage;
}
/** Returns the response message, if any, for this connection. */
public Message getResponseMessage() {
return responseMessage;
}
/*
* Package-friendly setters
*/
void setTransportUri(URLName transportUri) {
this.transportUri = transportUri;
}
void setFrom(InternetAddress from) {
this.from = from;
}
/*
* Errors
*/
public String getErrorMessage() throws IOException {
return null;
}
@@ -76,10 +107,6 @@ public class MailReceiverConnection extends AbstractReceiverConnection {
return false;
}
public void setTransportUri(URLName transportUri) {
this.transportUri = transportUri;
}
public void close() throws IOException {
}
@@ -167,14 +194,10 @@ public class MailReceiverConnection extends AbstractReceiverConnection {
throw new MailTransportException(ex);
}
finally {
MailUtils.closeService(transport);
MailTransportUtils.closeService(transport);
}
}
public void setFrom(InternetAddress from) {
this.from = from;
}
private class ByteArrayDataSource implements DataSource {
private byte[] data;

View File

@@ -0,0 +1,318 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Header;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.Transport;
import javax.mail.URLName;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.search.HeaderTerm;
import javax.mail.search.SearchTerm;
import org.springframework.util.Assert;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.transport.AbstractSenderConnection;
import org.springframework.ws.transport.TransportConstants;
import org.springframework.ws.transport.WebServiceConnection;
import org.springframework.ws.transport.mail.support.MailTransportUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Implementation of {@link WebServiceConnection} that is used for client-side Mail access. Exposes a {@link Message}
* request and response message.
*
* @author Arjen Poutsma
* @since 1.1.0
*/
public class MailSenderConnection extends AbstractSenderConnection {
private static final Log logger = LogFactory.getLog(MailSenderConnection.class);
private final Session session;
private MimeMessage requestMessage;
private Message responseMessage;
private String requestContentType;
private boolean deleteAfterReceive = false;
private final URLName storeUri;
private final URLName transportUri;
private ByteArrayOutputStream requestBuffer;
private InternetAddress from;
private final InternetAddress to;
private String subject;
private final long receiveTimeout;
private Store store;
private Folder folder;
/** Constructs a new Mail connection with the given parameters. */
protected MailSenderConnection(Session session,
URLName transportUri,
URLName storeUri,
InternetAddress to,
long receiveTimeout) {
Assert.notNull(session, "'session' must not be null");
Assert.notNull(transportUri, "'transportUri' must not be null");
Assert.notNull(storeUri, "'storeUri' must not be null");
Assert.notNull(to, "'to' must not be null");
this.session = session;
this.transportUri = transportUri;
this.storeUri = storeUri;
this.to = to;
this.receiveTimeout = receiveTimeout;
}
/** Returns the request message for this connection. */
public Message getRequestMessage() {
return requestMessage;
}
/** Returns the response message, if any, for this connection. */
public Message getResponseMessage() {
return responseMessage;
}
/*
* Package-friendly setters
*/
void setFrom(InternetAddress from) {
this.from = from;
}
void setSubject(String subject) {
this.subject = subject;
}
/*
* Sending
*/
protected void onSendBeforeWrite(WebServiceMessage message) throws IOException {
try {
requestMessage = new MimeMessage(session);
requestMessage.setRecipient(Message.RecipientType.TO, to);
requestMessage.setSentDate(new Date());
if (from != null) {
requestMessage.setFrom(from);
}
if (subject != null) {
requestMessage.setSubject(subject);
}
requestBuffer = new ByteArrayOutputStream();
}
catch (MessagingException ex) {
throw new MailTransportException(ex);
}
}
protected void addRequestHeader(String name, String value) throws IOException {
try {
requestMessage.addHeader(name, value);
if (TransportConstants.HEADER_CONTENT_TYPE.equals(name)) {
requestContentType = value;
}
}
catch (MessagingException ex) {
throw new MailTransportException(ex);
}
}
protected OutputStream getRequestOutputStream() throws IOException {
return requestBuffer;
}
protected void onSendAfterWrite(WebServiceMessage message) throws IOException {
Transport transport = null;
try {
requestMessage.setDataHandler(
new DataHandler(new ByteArrayDataSource(requestContentType, requestBuffer.toByteArray())));
transport = session.getTransport(transportUri);
transport.connect();
requestMessage.saveChanges();
transport.sendMessage(requestMessage, requestMessage.getAllRecipients());
}
catch (MessagingException ex) {
throw new MailTransportException(ex);
}
finally {
MailTransportUtils.closeService(transport);
}
}
/*
* Receiving
*/
protected void onReceiveBeforeRead() throws IOException {
try {
String requestMessageId = requestMessage.getMessageID();
Assert.hasLength(requestMessageId, "No Message-ID found on request message [" + requestMessage + "]");
try {
Thread.sleep(receiveTimeout);
}
catch (InterruptedException e) {
// Re-interrupt current thread, to allow other threads to react.
Thread.currentThread().interrupt();
}
openFolder();
SearchTerm searchTerm = new HeaderTerm(MailTransportConstants.HEADER_IN_REPLY_TO, requestMessageId);
Message[] responses = folder.search(searchTerm);
if (responses.length > 0) {
if (responses.length > 1) {
logger.warn("Received more than one response for request with ID [" + requestMessageId + "]");
}
responseMessage = responses[0];
}
if (deleteAfterReceive) {
responseMessage.setFlag(Flags.Flag.DELETED, true);
}
}
catch (MessagingException ex) {
throw new MailTransportException(ex);
}
}
private void openFolder() throws MessagingException {
store = session.getStore(storeUri);
store.connect();
folder = store.getFolder(storeUri);
if (folder == null || !folder.exists()) {
throw new IllegalStateException("No default folder to receive from");
}
if (deleteAfterReceive) {
folder.open(Folder.READ_WRITE);
}
else {
folder.open(Folder.READ_ONLY);
}
}
protected boolean hasResponse() throws IOException {
return responseMessage != null;
}
protected Iterator getResponseHeaderNames() throws IOException {
try {
List headers = new ArrayList();
Enumeration enumeration = responseMessage.getAllHeaders();
while (enumeration.hasMoreElements()) {
Header header = (Header) enumeration.nextElement();
headers.add(header.getName());
}
return headers.iterator();
}
catch (MessagingException ex) {
throw new MailTransportException(ex);
}
}
protected Iterator getResponseHeaders(String name) throws IOException {
try {
String[] headers = responseMessage.getHeader(name);
return Arrays.asList(headers).iterator();
}
catch (MessagingException ex) {
throw new MailTransportException(ex);
}
}
protected InputStream getResponseInputStream() throws IOException {
try {
return responseMessage.getDataHandler().getInputStream();
}
catch (MessagingException ex) {
throw new MailTransportException(ex);
}
}
public boolean hasError() throws IOException {
return false;
}
public String getErrorMessage() throws IOException {
return null;
}
public void close() throws IOException {
MailTransportUtils.closeFolder(folder, deleteAfterReceive);
MailTransportUtils.closeService(store);
}
private class ByteArrayDataSource implements DataSource {
private byte[] data;
private String contentType;
public ByteArrayDataSource(String contentType, byte[] data) {
this.data = data;
this.contentType = contentType;
}
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(data);
}
public OutputStream getOutputStream() throws IOException {
throw new UnsupportedOperationException();
}
public String getContentType() {
return contentType;
}
public String getName() {
return "ByteArrayDataSource";
}
}
}

View File

@@ -19,14 +19,16 @@ package org.springframework.ws.transport.mail;
import org.springframework.ws.transport.TransportConstants;
/**
* Declares Mail-specific transport constants.
*
* @author Arjen Poutsma
* @since 1.1.0
*/
public interface MailTransportConstants extends TransportConstants {
/**
* The "In-Reply-To" header.
*/
String HEADER_IN_REPLY_TO = "In-Reply-To";
/** The "mail" URI scheme. */
String MAIL_URI_SCHEME = "mailto";
String URI_SCHEME = "mailto";
/** The "In-Reply-To" header. */
String HEADER_IN_REPLY_TO = "In-Reply-To";
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail;
import javax.mail.MessagingException;
import org.springframework.ws.transport.TransportException;
/**
* Exception that is thrown when an error occurs in the Mail transport.
*
* @author Arjen Poutsma
* @since 1.1.0
*/
public class MailTransportException extends TransportException {
private final MessagingException messagingException;
public MailTransportException(String msg, MessagingException ex) {
super(msg + ": " + ex.getMessage());
initCause(ex);
messagingException = ex;
}
public MailTransportException(MessagingException ex) {
super(ex.getMessage());
initCause(ex);
messagingException = ex;
}
public MessagingException getMessagingException() {
return messagingException;
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail.monitor;
import javax.mail.FetchProfile;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.search.AndTerm;
import javax.mail.search.FlagTerm;
import javax.mail.search.SearchTerm;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Abstract base class for the {@link MonitoringStrategy} interface. Exposes a {@link #setDeleteMessages(boolean)
* deleteMessages} property, and includes a basic workflow for message monitoring.
*
* @author Arjen Poutsma
* @since 1.1.0
*/
public abstract class AbstractMonitoringStrategy implements MonitoringStrategy {
/** Logger available to subclasses. */
protected final Log logger = LogFactory.getLog(getClass());
private boolean deleteMessages = true;
/**
* Sets whether messages should be marked as {@link Flags.Flag#DELETED DELETED} after they have been read. Default
* is <code>true</code>.
*/
public void setDeleteMessages(boolean deleteMessages) {
this.deleteMessages = deleteMessages;
}
public int getFolderOpenMode() {
return deleteMessages ? Folder.READ_WRITE : Folder.READ_ONLY;
}
/**
* Monitors the given folder, and returns any new messages when they arrive. This implementation calls {@link
* #waitForNewMessages(Folder)}, then searches for new messages using {@link #searchForNewMessages(Folder)}, fetches
* the messages using {@link #fetchMessages(Folder, Message[])}, and finally {@link #setDeleteMessages(boolean)
* deletes} the messages, if {@link #setDeleteMessages(boolean) deleteMessages} is <code>true</code>.
*
* @param folder the folder to monitor
* @return the new messages
* @throws MessagingException in case of JavaMail errors
* @throws InterruptedException when a thread is interrupted
*/
public final Message[] monitor(Folder folder) throws MessagingException, InterruptedException {
waitForNewMessages(folder);
Message[] messages = searchForNewMessages(folder);
if (logger.isDebugEnabled()) {
logger.debug("Found " + messages.length + " new messages");
}
if (messages.length > 0) {
fetchMessages(folder, messages);
}
if (deleteMessages) {
deleteMessages(folder, messages);
}
return messages;
}
/**
* Template method that blocks until new messages arrive in the given folder. Typical implementations use {@link
* Thread#sleep(long)} or the IMAP IDLE command.
*
* @param folder the folder to monitor
* @throws MessagingException in case of JavaMail errors
* @throws InterruptedException when a thread is interrupted
*/
protected abstract void waitForNewMessages(Folder folder) throws MessagingException, InterruptedException;
/**
* Retrieves new messages from the given folder. This implementation creates a {@link SearchTerm} that searches for
* all messages in the folder that are {@link Flags.Flag#RECENT RECENT}, not {@link Flags.Flag#ANSWERED ANSWERED},
* and not {@link Flags.Flag#DELETED DELETED}. The search term is used to {@link Folder#search(SearchTerm) search}
* for new messages.
*
* @param folder the folder to retrieve new messages from
* @return the new messages
* @throws MessagingException in case of JavaMail errors
*/
protected Message[] searchForNewMessages(Folder folder) throws MessagingException {
if (!folder.isOpen()) {
return new Message[0];
}
Flags supportedFlags = folder.getPermanentFlags();
SearchTerm searchTerm = null;
if (supportedFlags.contains(Flags.Flag.RECENT)) {
searchTerm = new FlagTerm(new Flags(Flags.Flag.RECENT), true);
}
if (supportedFlags.contains(Flags.Flag.ANSWERED)) {
FlagTerm answeredTerm = new FlagTerm(new Flags(Flags.Flag.ANSWERED), false);
if (searchTerm == null) {
searchTerm = answeredTerm;
}
else {
searchTerm = new AndTerm(searchTerm, answeredTerm);
}
}
if (supportedFlags.contains(Flags.Flag.DELETED)) {
FlagTerm deletedTerm = new FlagTerm(new Flags(Flags.Flag.DELETED), false);
if (searchTerm == null) {
searchTerm = deletedTerm;
}
else {
searchTerm = new AndTerm(searchTerm, deletedTerm);
}
}
return searchTerm != null ? folder.search(searchTerm) : folder.getMessages();
}
/**
* Fetches the specified messages from the specified folder. Default implementation {@link Folder#fetch(Message[],
* FetchProfile) fetches} every {@link FetchProfile.Item}.
*
* @param folder the folder to fetch messages from
* @param messages the messages to fetch
* @throws MessagingException in case of JavMail errors
*/
protected void fetchMessages(Folder folder, Message[] messages) throws MessagingException {
FetchProfile contentsProfile = new FetchProfile();
contentsProfile.add(FetchProfile.Item.ENVELOPE);
contentsProfile.add(FetchProfile.Item.CONTENT_INFO);
contentsProfile.add(FetchProfile.Item.FLAGS);
folder.fetch(messages, contentsProfile);
}
/**
* Deletes the given messages from the given folder. Only invoked when {@link #setDeleteMessages(boolean)} is
* <code>true</code>.
*
* @param folder the folder to delete messages from
* @param messages the messages to delete
* @throws MessagingException in case of JavaMail errors
*/
protected void deleteMessages(Folder folder, Message[] messages) throws MessagingException {
for (int i = 0; i < messages.length; i++) {
messages[i].setFlag(Flags.Flag.DELETED, true);
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail.monitor;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.event.MessageCountAdapter;
import javax.mail.event.MessageCountEvent;
import javax.mail.event.MessageCountListener;
import org.springframework.util.Assert;
import com.sun.mail.imap.IMAPFolder;
/**
* Implementation of the {@link MonitoringStrategy} interface that uses the IMAP IDLE command for asynchronous message
* detection.
* <p/>
* <b>Note</b> that this implementation is only suitable for use with IMAP servers which support the IDLE command.
* Additionally, this strategy requires JavaMail version 1.4.1.
*
* @author Arjen Poutsma
* @since 1.1.0
*/
public class ImapIdleMonitoringStrategy extends AbstractMonitoringStrategy {
private MessageCountListener messageCountListener;
protected void waitForNewMessages(Folder folder) throws MessagingException, InterruptedException {
Assert.isInstanceOf(IMAPFolder.class, folder);
IMAPFolder imapFolder = (IMAPFolder) folder;
// retrieve unseen messages before we enter the blocking idle call
if (searchForNewMessages(folder).length > 0) {
return;
}
if (messageCountListener == null) {
createMessageCountListener();
}
folder.addMessageCountListener(messageCountListener);
try {
imapFolder.idle();
}
finally {
folder.removeMessageCountListener(messageCountListener);
}
}
private void createMessageCountListener() {
messageCountListener = new MessageCountAdapter() {
public void messagesAdded(MessageCountEvent e) {
Message[] messages = e.getMessages();
for (int i = 0; i < messages.length; i++) {
try {
// this will return the flow to the idle call, above
messages[i].getLineCount();
}
catch (MessagingException ex) {
// ignore
}
}
}
};
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail.monitor;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
/**
* Defines the contract for objects that monitor a given folder for new messages. Allows for multiple implementation
* strategies, including polling, or event-driven techniques such as IMAP's <code>IDLE</code> command.
*
* @author Arjen Poutsma
*/
public interface MonitoringStrategy {
/**
* Monitors the given folder, and returns any new messages when they arrive.
*
* @param folder the folder in which to look for new messages
* @return the new messages
* @throws MessagingException in case of JavaMail errors
* @throws InterruptedException if a thread is interrupted
*/
Message[] monitor(Folder folder) throws MessagingException, InterruptedException;
/**
* Returns the folder open mode to be used by this strategy. Can be either {@link Folder#READ_ONLY} or {@link
* Folder#READ_WRITE}.
*/
int getFolderOpenMode();
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail.monitor;
import javax.mail.Folder;
import javax.mail.MessagingException;
/**
* Implementation of the {@link MonitoringStrategy} interface that uses a simple polling mechanism. Defines a {@link
* #setPollingInterval(long) polling interval} property which defines the interval in between message polls.
* <p/>
* <b>Note</b> that this implementation is not suitable for use with POP3 servers. Use the {@link
* Pop3PollingMonitoringStrategy} instead.
*
* @author Arjen Poutsma
* @since 1.1.0
*/
public class PollingMonitoringStrategy extends AbstractMonitoringStrategy {
/** Defines the default polling frequency. Set to 1000 * 60 milliseconds (i.e. 1 minute). */
public static final long DEFAULT_POLLING_FREQUENCY = 1000 * 60;
private long pollingInterval = DEFAULT_POLLING_FREQUENCY;
/**
* Sets the interval used in between message polls, <strong>in milliseconds</strong>. The default is 1000 * 60 ms,
* that is 1 minute.
*/
public void setPollingInterval(long pollingInterval) {
this.pollingInterval = pollingInterval;
}
protected void waitForNewMessages(Folder folder) throws MessagingException, InterruptedException {
Thread.sleep(pollingInterval);
afterSleep(folder);
}
/**
* Invoked after the {@link Thread#sleep(long)} method has been invoked. This implementation calls {@link
* Folder#getMessageCount(), to force new messages to be seen.
*
* @param folder the folder to check for new messages
* @throws MessagingException in case of JavaMail errors
*/
protected void afterSleep(Folder folder) throws MessagingException {
folder.getMessageCount();
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail.monitor;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.springframework.ws.transport.mail.support.MailTransportUtils;
/**
* Implementation of the {@link MonitoringStrategy} interface that uses a simple polling mechanism suitable for POP3
* servers. Since POP3 does not have a native mechanism to determine which messages are "new", this implementation
* simply retrieves all messages in the {@link Folder}, and delete them afterwards. All messages in the POP3 mailbox are
* therefore, by definition, new.
* <p/>
* Setting the {@link #setDeleteMessages(boolean) deleteMessages} property is therefore ignored: messages are always
* deleted.
*
* @author Arjen Poutsma
* @since 1.1.0
*/
public class Pop3PollingMonitoringStrategy extends PollingMonitoringStrategy {
public Pop3PollingMonitoringStrategy() {
super.setDeleteMessages(true);
}
public void setDeleteMessages(boolean deleteMessages) {
}
/** Re-opens the folder, if it closed. */
protected void afterSleep(Folder folder) throws MessagingException {
if (!folder.isOpen()) {
folder.open(Folder.READ_WRITE);
}
}
/** Simply returns {@link Folder#getMessages()}. */
protected Message[] searchForNewMessages(Folder folder) throws MessagingException {
return folder.getMessages();
}
/**
* Deletes the given messages from the given folder, and closes it to expunge deleted messages.
*
* @param folder the folder to delete messages from
* @param messages the messages to delete
* @throws MessagingException in case of JavaMail errors
*/
protected void deleteMessages(Folder folder, Message[] messages) throws MessagingException {
super.deleteMessages(folder, messages);
// expunge deleted mails, and make sure we've retrieved them before closing the folder
for (int i = 0; i < messages.length; i++) {
new MimeMessage((MimeMessage) messages[i]);
}
MailTransportUtils.closeFolder(folder, true);
}
}

View File

@@ -0,0 +1,6 @@
<html>
<body>
Provides the MonitoringStrategy interface and implementations. Used for monitoring a JavaMail Folder for new email
messages.
</body>
</html>

View File

@@ -0,0 +1,5 @@
<html>
<body>
Package providing support for handling messages via email.
</body>
</html>

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2007 the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail.support;
import java.net.URI;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.mail.Folder;
import javax.mail.MessagingException;
import javax.mail.Service;
import javax.mail.Store;
import javax.mail.Transport;
import javax.mail.URLName;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import org.springframework.util.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Collection of utility methods to work with Mail transports.
*
* @author Arjen Poutsma
* @since 1.1.0
*/
public abstract class MailTransportUtils {
private static final Pattern TO_PATTERN = Pattern.compile("^([^\\?]+)");
private static final Pattern SUBJECT_PATTERN = Pattern.compile("subject=([^\\&]+)");
private static final Log logger = LogFactory.getLog(MailTransportUtils.class);
private MailTransportUtils() {
}
public static InternetAddress getTo(URI uri) {
Matcher matcher = TO_PATTERN.matcher(uri.getSchemeSpecificPart());
if (matcher.find()) {
for (int i = 1; i <= matcher.groupCount(); i++) {
String group = matcher.group(i);
if (group != null) {
try {
return new InternetAddress(group);
}
catch (AddressException e) {
// try next group
}
}
}
}
return null;
}
public static String getSubject(URI uri) {
Matcher matcher = SUBJECT_PATTERN.matcher(uri.getSchemeSpecificPart());
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
/**
* Close the given JavaMail Service and ignore any thrown exception. This is useful for typical <code>finally</code>
* blocks in manual JavaMail code.
*
* @param service the JavaMail Service to close (may be <code>null</code>)
* @see Transport
* @see Store
*/
public static void closeService(Service service) {
if (service != null) {
try {
service.close();
}
catch (MessagingException ex) {
logger.debug("Could not close JavaMail Service", ex);
}
}
}
/**
* Close the given JavaMail Folder and ignore any thrown exception. This is useful for typical <code>finally</code>
* blocks in manual JavaMail code.
*
* @param folder the JavaMail Folder to close (may be <code>null</code>)
*/
public static void closeFolder(Folder folder) {
closeFolder(folder, false);
}
/**
* Close the given JavaMail Folder and ignore any thrown exception. This is useful for typical <code>finally</code>
* blocks in manual JavaMail code.
*
* @param folder the JavaMail Folder to close (may be <code>null</code>)
* @param expunge whether all deleted messages should be expunged from the folder
*/
public static void closeFolder(Folder folder, boolean expunge) {
if (folder != null && folder.isOpen()) {
try {
folder.close(expunge);
}
catch (MessagingException ex) {
logger.debug("Could not close JavaMail Folder", ex);
}
}
}
/** Returns a string representation of the given {@link URLName}, where the password has been protected. */
public static String toPasswordProtectedString(URLName name) {
String protocol = name.getProtocol();
String username = name.getUsername();
String password = name.getPassword();
String host = name.getHost();
int port = name.getPort();
String file = name.getFile();
String ref = name.getRef();
StringBuffer tempURL = new StringBuffer();
if (protocol != null) {
tempURL.append(protocol).append(':');
}
if (StringUtils.hasLength(username) || StringUtils.hasLength(null)) {
tempURL.append("//");
if (StringUtils.hasLength(username)) {
tempURL.append(username);
if (StringUtils.hasLength(password)) {
tempURL.append(":*****");
}
tempURL.append("@");
}
if (StringUtils.hasLength(host)) {
tempURL.append(host);
}
if (port != -1) {
tempURL.append(':').append(Integer.toString(port));
}
if (StringUtils.hasLength(file)) {
tempURL.append('/');
}
}
if (StringUtils.hasLength(file)) {
tempURL.append(file);
}
if (StringUtils.hasLength(ref)) {
tempURL.append('#').append(ref);
}
return tempURL.toString();
}
}

View File

@@ -0,0 +1,5 @@
<html>
<body>
Classes supporting the org.springframework.ws.transport.mail package.
</body>
</html>

View File

@@ -19,16 +19,16 @@ package org.springframework.ws.transport.support;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.util.ClassUtils;
import org.springframework.scheduling.commonj.WorkManagerTaskExecutor;
import org.springframework.util.ClassUtils;
/**
* Abstract base class for standalone, server-side transport objects. Contains a Spring {@link TaskExecutor}, and
* various lifecycle callbacks.
* Abstract base class for asynchronous standalone, server-side transport objects. Contains a Spring {@link
* TaskExecutor}, and various lifecycle callbacks.
*
* @author Arjen Poutsma
*/
public abstract class AbstractMultiThreadedMessageReceiver extends AbstractStandaloneMessagingReceiver
public abstract class AbstractAsyncStandaloneMessageReceiver extends AbstractStandaloneMessageReceiver
implements BeanNameAware {
/** Default thread name prefix. */
@@ -38,11 +38,6 @@ public abstract class AbstractMultiThreadedMessageReceiver extends AbstractStand
private String beanName;
/** Returns the task executor. */
public TaskExecutor getTaskExecutor() {
return taskExecutor;
}
/**
* Set the Spring {@link TaskExecutor} to use for running the listener threads. Default is {@link
* SimpleAsyncTaskExecutor}, starting up a number of new threads.
@@ -77,4 +72,13 @@ public abstract class AbstractMultiThreadedMessageReceiver extends AbstractStand
String threadNamePrefix = beanName != null ? beanName + "-" : DEFAULT_THREAD_NAME_PREFIX;
return new SimpleAsyncTaskExecutor(threadNamePrefix);
}
/**
* Executes the given {@link Runnable} via this receiver's {@link TaskExecutor}.
*
* @see #setTaskExecutor(TaskExecutor)
*/
protected void execute(Runnable runnable) {
taskExecutor.execute(runnable);
}
}

View File

@@ -19,8 +19,14 @@ package org.springframework.ws.transport.support;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.Lifecycle;
/** @author Arjen Poutsma */
public abstract class AbstractStandaloneMessagingReceiver extends SimpleWebServiceMessageReceiverObjectSupport
/**
* Abstract base class for standalone, server-side transport objects. Provides a basic, thread-safe implementation of
* the {@link Lifecycle} interface, and various template methods to be implemented by concrete sub classes.
*
* @author Arjen Poutsma
* @since 1.1.0
*/
public abstract class AbstractStandaloneMessageReceiver extends SimpleWebServiceMessageReceiverObjectSupport
implements Lifecycle, DisposableBean {
private volatile boolean active = false;
@@ -54,20 +60,20 @@ public abstract class AbstractStandaloneMessagingReceiver extends SimpleWebServi
this.autoStartup = autoStartup;
}
/** Calls {@link #activate()} when the BeanFactory initializes the receiver instance. */
public void afterPropertiesSet() throws Exception {
activate();
}
/**
* Calls <code>shutdown</code> when the BeanFactory destroys the server instance.
*
* @see #shutdown()
*/
/** Calls {@link #shutdown()} when the BeanFactory destroys the receiver instance. */
public void destroy() {
shutdown();
}
/** Initialize this server. Starts the server if <code>autoStartup</code> hasn't been turned off. */
/**
* Initialize this server. Starts the server if {@link #setAutoStartup(boolean) autoStartup} hasn't been turned
* off.
*/
public final void activate() throws Exception {
synchronized (lifecycleMonitor) {
active = true;
@@ -97,7 +103,7 @@ public abstract class AbstractStandaloneMessagingReceiver extends SimpleWebServi
onStop();
}
/** Shut down the registered listeners and close this listener container. */
/** Shut down this server. */
public final void shutdown() {
synchronized (lifecycleMonitor) {
running = false;
@@ -107,11 +113,19 @@ public abstract class AbstractStandaloneMessagingReceiver extends SimpleWebServi
onShutdown();
}
/**
* Template method invoked when {@link #activate()} is invoked.
*
* @throws Exception in case of errors
*/
protected abstract void onActivate() throws Exception;
/** Template method invoked when {@link #start()} is invoked. */
protected abstract void onStart();
/** Template method invoked when {@link #stop()} is invoked. */
protected abstract void onStop();
/** Template method invoked when {@link #shutdown()} is invoked. */
protected abstract void onShutdown();
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright ${YEAR} the original author or authors.
*
* Licensed 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.springframework.ws.transport.mail.support;
import java.net.URI;
import javax.mail.URLName;
import javax.mail.internet.InternetAddress;
import junit.framework.TestCase;
public class MailTransportUtilsTest extends TestCase {
public void testToPasswordProtectedString() throws Exception {
URLName name = new URLName("imap://john:secret@imap.example.com/INBOX");
String result = MailTransportUtils.toPasswordProtectedString(name);
assertEquals("Password found in string", -1, result.indexOf("secret"));
}
public void testGetTo() throws Exception {
URI uri = new URI("mailto:infobot@example.com?subject=current-issue");
InternetAddress to = MailTransportUtils.getTo(uri);
assertEquals("Invalid destination", new InternetAddress("infobot@example.com"), to);
uri = new URI("mailto:infobot@example.com");
to = MailTransportUtils.getTo(uri);
assertEquals("Invalid destination", new InternetAddress("infobot@example.com"), to);
}
public void testGetSubject() throws Exception {
URI uri = new URI("mailto:infobot@example.com?subject=current-issue");
String subject = MailTransportUtils.getSubject(uri);
assertEquals("Invalid destination", "current-issue", subject);
uri = new URI("mailto:infobot@example.com");
subject = MailTransportUtils.getSubject(uri);
assertNull("Invalid destination", subject);
}
}