diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/AbstractMailReceiver.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/AbstractMailReceiver.java new file mode 100755 index 0000000000..35099e4460 --- /dev/null +++ b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/AbstractMailReceiver.java @@ -0,0 +1,217 @@ +/* + * Copyright 2002-2008 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.integration.mail; + +import java.util.Properties; + +import javax.mail.FetchProfile; +import javax.mail.Flags; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.URLName; +import javax.mail.internet.MimeMessage; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.integration.mail.monitor.MailTransportUtils; +import org.springframework.util.Assert; + +/** + * Base class for {@link MailReceiver} implementations. + * + * @author Arjen Poutsma + * @author Jonas Partner + * @author Mark Fisher + */ +public abstract class AbstractMailReceiver implements MailReceiver, DisposableBean { + + protected final Log logger = LogFactory.getLog(this.getClass()); + + private final URLName url; + + private volatile int maxFetchSize = -1; + + private volatile Session session; + + private volatile Store store; + + private volatile Folder folder; + + private volatile Properties javaMailProperties = new Properties(); + + protected volatile boolean initialized; + + private final Object initializationMonitor = new Object(); + + + public AbstractMailReceiver(URLName urlName) { + Assert.notNull(urlName, "urlName must not be null"); + this.url = urlName; + } + + public AbstractMailReceiver(String url) { + Assert.notNull(url, "url must not be null"); + this.url = new URLName(url); + } + + + public void setJavaMailProperties(Properties javaMailProperties) { + this.javaMailProperties = javaMailProperties; + } + + public void setMaxFetchSize(int maxFetchSize) { + this.maxFetchSize = maxFetchSize; + } + + protected Folder getFolder() { + return this.folder; + } + + /** + * Subclasses must implement this method to return new mail messages. + */ + protected abstract Message[] searchForNewMessages() throws MessagingException; + + /** + * Subclasses must implement this method to indicate whether the mail + * messages should be deleted after being received. + */ + protected abstract boolean shouldDeleteMessages(); + + private void openSession() throws MessagingException { + if (this.session == null) { + this.session = Session.getInstance(this.javaMailProperties); + } + if (this.store == null) { + this.store = this.session.getStore(this.url); + } + if (!this.store.isConnected()) { + if (logger.isDebugEnabled()) { + logger.debug("connecting to store [" + MailTransportUtils.toPasswordProtectedString(this.url) + "]"); + } + this.store.connect(); + } + } + + protected void openFolder() throws MessagingException { + this.openSession(); + if (this.folder == null) { + this.folder = this.store.getFolder(this.url); + } + if (this.folder == null || !this.folder.exists()) { + throw new IllegalStateException("no such folder [" + this.url.getFile() + "]"); + } + if (this.folder.isOpen()) { + return; + } + if (logger.isDebugEnabled()) { + logger.debug("opening folder [" + MailTransportUtils.toPasswordProtectedString(this.url) + "]"); + } + if (this.shouldDeleteMessages()) { + this.folder.open(Folder.READ_WRITE); + } + else { + this.folder.open(Folder.READ_ONLY); + } + } + + public synchronized Message[] receive() { + try { + this.openFolder(); + if (logger.isInfoEnabled()) { + logger.info("attempting to receive mail from folder [" + this.getFolder().getFullName() + "]"); + } + Message[] messages = this.searchForNewMessages(); + if (this.maxFetchSize > 0 && messages.length > this.maxFetchSize) { + Message[] reducedMessages = new Message[this.maxFetchSize]; + System.arraycopy(messages, 0, reducedMessages, 0, this.maxFetchSize); + messages = reducedMessages; + } + if (logger.isDebugEnabled()) { + logger.debug("found " + messages.length + " new messages"); + } + if (messages.length > 0) { + this.fetchMessages(messages); + } + if (this.shouldDeleteMessages()) { + this.deleteMessages(messages); + } + Message[] copiedMessages = new Message[messages.length]; + for (int i = 0; i < messages.length; i++) { + copiedMessages[i] = new MimeMessage((MimeMessage) messages[i]); + } + return copiedMessages; + } + catch (Exception e) { + throw new org.springframework.integration.core.MessagingException( + "failure occurred while receiving from folder", e); + } + finally { + MailTransportUtils.closeFolder(this.folder); + } + } + + /** + * Fetches the specified messages from this receiver's folder. Default + * implementation {@link Folder#fetch(Message[], FetchProfile) fetches} + * every {@link javax.mail.FetchProfile.Item}. + * + * @param messages the messages to fetch + * @throws MessagingException in case of JavMail errors + */ + protected void fetchMessages(Message[] messages) throws MessagingException { + FetchProfile contentsProfile = new FetchProfile(); + contentsProfile.add(FetchProfile.Item.ENVELOPE); + contentsProfile.add(FetchProfile.Item.CONTENT_INFO); + contentsProfile.add(FetchProfile.Item.FLAGS); + this.folder.fetch(messages, contentsProfile); + } + + /** + * Deletes the given messages from this receiver's folder. Only invoked + * when {@link #setDeleteMessages(boolean)} is true. + * + * @param messages the messages to delete + * @throws MessagingException in case of JavaMail errors + */ + protected void deleteMessages(Message[] messages) throws MessagingException { + for (int i = 0; i < messages.length; i++) { + messages[i].setFlag(Flags.Flag.DELETED, true); + } + } + + public void destroy() throws Exception { + synchronized (this.initializationMonitor) { + MailTransportUtils.closeFolder(this.folder); + MailTransportUtils.closeService(this.store); + this.folder = null; + this.store = null; + this.initialized = false; + } + } + + @Override + public String toString() { + return this.url.toString(); + } + +} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/DefaultFolderConnection.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/DefaultFolderConnection.java deleted file mode 100644 index 22cbe75f0a..0000000000 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/DefaultFolderConnection.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2002-2008 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.integration.mail; - -import java.util.Properties; - -import javax.mail.Folder; -import javax.mail.Message; -import javax.mail.MessagingException; -import javax.mail.Session; -import javax.mail.Store; -import javax.mail.URLName; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.context.Lifecycle; -import org.springframework.integration.mail.monitor.AsyncMonitoringStrategy; -import org.springframework.integration.mail.monitor.MailTransportUtils; -import org.springframework.integration.mail.monitor.MonitoringStrategy; -import org.springframework.util.Assert; - -/** - * A Connection to a mail folder capable of retrieving mail by utilizing the - * given instance of {@link MonitoringStrategy}. - * - * @author Jonas Partner - */ -public class DefaultFolderConnection implements Lifecycle, DisposableBean, FolderConnection { - - private final Log logger = LogFactory.getLog(this.getClass()); - - private final URLName storeUri; - - private final MonitoringStrategy monitoringStrategy; - - private final boolean polling; - - private volatile Session session; - - private volatile Store store; - - private volatile Folder folder; - - private volatile Properties javaMailProperties = new Properties(); - - private volatile boolean running; - - private final Object lifecycleMonitor = new Object(); - - - public DefaultFolderConnection(String storeUri, MonitoringStrategy monitoringStrategy, boolean polling) { - Assert.notNull(storeUri, "storeUri must not be null"); - Assert.notNull(monitoringStrategy, "monitoringStrategy must not ne null"); - this.storeUri = new URLName(storeUri); - this.monitoringStrategy = monitoringStrategy; - this.polling = polling; - Assert.isTrue(polling || AsyncMonitoringStrategy.class.isAssignableFrom(monitoringStrategy.getClass()), - "Folder connection requires an AsyncMonitoringStrategy if polling is disabled."); - } - - - public void setJavaMailProperties(Properties javaMailProperties) { - this.javaMailProperties = javaMailProperties; - } - - public synchronized Message[] receive() { - try { - if (!this.isRunning()) { - this.start(); - } - if (!this.polling) { - ((AsyncMonitoringStrategy) this.monitoringStrategy).waitForNewMessages(this.folder); - } - return this.monitoringStrategy.receive(this.folder); - } - catch (Exception e) { - throw new org.springframework.integration.core.MessagingException( - "failure occurred while receiving from folder", e); - } - } - - @Override - public String toString() { - return this.storeUri.toString(); - } - - public void destroy() throws Exception { - this.stop(); - } - - /* - * Lifecycle implementation - */ - - public boolean isRunning() { - synchronized (this.lifecycleMonitor) { - return this.running; - } - } - - public synchronized void start() { - synchronized (this.lifecycleMonitor) { - try { - this.openSession(); - this.openFolder(); - this.running = true; - } - catch (MessagingException e) { - throw new org.springframework.integration.core.MessagingException( - "Failed to start FolderConnection", e); - } - } - } - - public synchronized void stop() { - synchronized (this.lifecycleMonitor) { - MailTransportUtils.closeFolder(this.folder); - MailTransportUtils.closeService(this.store); - this.folder = null; - this.store = null; - this.running = false; - } - } - - private void openFolder() throws MessagingException { - this.folder = this.store.getFolder(this.storeUri); - if (this.folder == null || !this.folder.exists()) { - throw new IllegalStateException("no default folder available"); - } - if (this.folder.isOpen()) { - return; - } - if (logger.isDebugEnabled()) { - logger.debug("Opening folder [" + MailTransportUtils.toPasswordProtectedString(this.storeUri) + "]"); - } - this.folder.open(this.monitoringStrategy.getFolderOpenMode()); - } - - private void openSession() throws MessagingException { - this.session = Session.getInstance(this.javaMailProperties); - this.store = this.session.getStore(this.storeUri); - if (logger.isDebugEnabled()) { - logger.debug("Connecting to store [" + MailTransportUtils.toPasswordProtectedString(this.storeUri) + "]"); - } - this.store.connect(); - } - -} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/ImapIdleChannelAdapter.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/ImapIdleChannelAdapter.java new file mode 100755 index 0000000000..b348f756b3 --- /dev/null +++ b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/ImapIdleChannelAdapter.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2008 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.integration.mail; + +import java.util.Properties; + +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + +import org.springframework.context.Lifecycle; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.integration.endpoint.AbstractMessageProducingEndpoint; +import org.springframework.integration.message.MessageBuilder; +import org.springframework.util.Assert; + +/** + * An event-driven Channel Adapter that receives mail messages from a mail + * server that supports the IMAP "idle" command (see RFC 2177). Received mail + * messages will be converted and sent as Spring Integration Messages to the + * output channel. The Message payload will be the {@link javax.mail.Message} + * instance that was received. + * + * @author Arjen Poutsma + * @author Mark Fisher + */ +public class ImapIdleChannelAdapter extends AbstractMessageProducingEndpoint implements Lifecycle { + + private final IdleTask idleTask = new IdleTask(); + + private volatile TaskExecutor taskExecutor; + + private volatile boolean running; + + private final Object lifecycleMonitor = new Object(); + + private final ImapMailReceiver mailReceiver; + + + public ImapIdleChannelAdapter(ImapMailReceiver mailReceiver) { + Assert.notNull(mailReceiver, "mailReceiver must not be null"); + this.mailReceiver = mailReceiver; + } + + public ImapIdleChannelAdapter(String url) { + Assert.isTrue(url.startsWith("imap"), "url must start with 'imap'"); + this.mailReceiver = new ImapMailReceiver(url); + } + + + public void setTaskExecutor(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + public void setJavaMailProperties(Properties javaMailProperties) { + this.mailReceiver.setJavaMailProperties(javaMailProperties); + } + + public void setShouldDeleteMessages(boolean shouldDeleteMessages) { + this.mailReceiver.setShouldDeleteMessages(shouldDeleteMessages); + } + + protected void handleMailMessagingException(MessagingException e) { + if (logger.isWarnEnabled()) { + logger.warn("error occurred in idle task", e); + } + } + + /* + * Lifecycle implementation + */ + + public boolean isRunning() { + return this.running; + } + + public void start() { + synchronized (this.lifecycleMonitor) { + if (!this.running) { + if (this.taskExecutor == null) { + if (logger.isInfoEnabled()) { + logger.info("No TaskExecutor has been provided, will use a [" + + SimpleAsyncTaskExecutor.class + "] as the default."); + } + this.taskExecutor = new SimpleAsyncTaskExecutor(); + } + this.taskExecutor.execute(this.idleTask); + } + this.running = true; + } + } + + public void stop() { + synchronized (this.lifecycleMonitor) { + if (this.running) { + this.idleTask.interrupt(); + } + this.running = false; + } + } + + + private class IdleTask implements Runnable { + + private volatile Thread thread; + + public synchronized void interrupt() { + if (this.thread != null) { + this.thread.interrupt(); + } + else if (logger.isInfoEnabled()) { + logger.info("monitor is not running, cannot interrupt"); + } + } + + public void run() { + this.thread = Thread.currentThread(); + while (!Thread.currentThread().isInterrupted()) { + try { + if (logger.isDebugEnabled()) { + logger.debug("waiting for mail"); + } + mailReceiver.waitForNewMessages(); + Message[] mailMessages = mailReceiver.receive(); + if (logger.isDebugEnabled()) { + logger.debug("received " + mailMessages.length + " mail messages"); + } + for (Message mailMessage : mailMessages) { + MimeMessage copied = new MimeMessage((MimeMessage) mailMessage); + sendMessage(MessageBuilder.withPayload(copied).build()); + } + } + catch (MessagingException e) { + handleMailMessagingException(e); + return; + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + } + +} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/ImapMailReceiver.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/ImapMailReceiver.java new file mode 100755 index 0000000000..6cd4d68314 --- /dev/null +++ b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/ImapMailReceiver.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2008 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.integration.mail; + +import javax.mail.Flags; +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 javax.mail.search.AndTerm; +import javax.mail.search.FlagTerm; +import javax.mail.search.SearchTerm; + +import org.springframework.integration.mail.monitor.MailTransportUtils; +import org.springframework.util.Assert; + +import com.sun.mail.imap.IMAPFolder; + +/** + * @author Arjen Poutsma + * @author Mark Fisher + */ +public class ImapMailReceiver extends AbstractMailReceiver { + + private volatile boolean shouldDeleteMessages = true; + + private final MessageCountListener messageCountListener = new SimpleMessageCountListener(); + + + public ImapMailReceiver(String url) { + super(url); + } + + + /** + * Specify whether mail messages should be deleted after retrieval. + * The default is true. + */ + public void setShouldDeleteMessages(boolean shouldDeleteMessages) { + this.shouldDeleteMessages = shouldDeleteMessages; + } + + @Override + protected boolean shouldDeleteMessages() { + return this.shouldDeleteMessages; + } + + /** + * This method is unique to the IMAP receiver and only works if IMAP IDLE + * is supported (see RFC 2177 for more detail). + */ + public void waitForNewMessages() throws MessagingException, InterruptedException { + this.openFolder(); + Assert.state(this.getFolder() instanceof IMAPFolder, + "folder is not an instance of [" + IMAPFolder.class.getName() + "]"); + IMAPFolder imapFolder = (IMAPFolder) this.getFolder(); + if (imapFolder.hasNewMessages()) { + return; + } + imapFolder.addMessageCountListener(this.messageCountListener); + try { + imapFolder.idle(); + } + finally { + imapFolder.removeMessageCountListener(this.messageCountListener); + } + } + + /** + * Retrieves new messages from this receiver's folder. This implementation + * creates a {@link SearchTerm} that searches for all messages in the + * folder that are {@link javax.mail.Flags.Flag#RECENT RECENT}, not + * {@link javax.mail.Flags.Flag#ANSWERED ANSWERED}, and not + * {@link javax.mail.Flags.Flag#DELETED DELETED}. The search term is used + * to {@link Folder#search(SearchTerm) search} for new messages. + * + * @return the new messages + * @throws MessagingException in case of JavaMail errors + */ + @Override + protected Message[] searchForNewMessages() throws MessagingException { + Flags supportedFlags = this.getFolder().getPermanentFlags(); + SearchTerm searchTerm = null; + if (supportedFlags != 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); + } + } + } + Message[] results = searchTerm != null ? this.getFolder().search(searchTerm) : this.getFolder().getMessages(); + if (results == null || results.length == 0) { + MailTransportUtils.closeFolder(this.getFolder()); + } + return results; + } + + + /** + * Callback used for handling the event-driven idle response. + */ + private static class SimpleMessageCountListener extends MessageCountAdapter { + + public void messagesAdded(MessageCountEvent event) { + Message[] messages = event.getMessages(); + for (Message message : messages) { + try { + // this will return the flow to the idle call + message.getLineCount(); + } + catch (MessagingException e) { + // ignored; + } + } + } + } + +} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/ListeningMailSource.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/ListeningMailSource.java deleted file mode 100644 index 97f029df69..0000000000 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/ListeningMailSource.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2002-2008 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.integration.mail; - -import javax.mail.Message; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.context.Lifecycle; -import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.core.task.TaskExecutor; -import org.springframework.integration.endpoint.AbstractMessageProducingEndpoint; -import org.springframework.integration.mail.monitor.AsyncMonitoringStrategy; -import org.springframework.integration.message.MessageBuilder; -import org.springframework.util.Assert; - -/** - * An event-driven mail source that sends Spring Integration Messages to its - * output channel. The Message payload will be the {@link javax.mail.Message} - * instance that was received. The given {@link FolderConnection} should be - * using an {@link AsyncMonitoringStrategy} to retrieve mail. - * - * @author Jonas Partner - * @author Mark Fisher - */ -public class ListeningMailSource extends AbstractMessageProducingEndpoint implements Lifecycle, DisposableBean { - - private final Log logger = LogFactory.getLog(this.getClass()); - - private volatile TaskExecutor taskExecutor; - - private final MonitorRunnable monitorRunnable; - - private volatile boolean monitorRunning = false; - - - public ListeningMailSource(FolderConnection folderConnection) { - Assert.notNull(folderConnection, "FolderConnection must not be null"); - this.monitorRunnable = new MonitorRunnable(folderConnection); - } - - - // Lifecycle implementation - - public boolean isRunning() { - return this.monitorRunning; - } - - public void start() { - this.startMonitor(); - if (logger.isInfoEnabled()) { - logger.info("started monitoring mailbox [" - + this.monitorRunnable.folderConnection + "]"); - } - } - - public void stop() { - this.stopMonitor(); - if (logger.isInfoEnabled()) { - logger.info("stopped monitoring mailbox [" - + this.monitorRunnable.folderConnection + "]"); - } - } - - protected void startMonitor() { - synchronized (this.monitorRunnable) { - if (!this.monitorRunning) { - if (this.taskExecutor == null) { - if (logger.isInfoEnabled()) { - logger.info("No TaskExecutor has been provided, will use a [" - + SimpleAsyncTaskExecutor.class + "] as the default."); - } - this.taskExecutor = new SimpleAsyncTaskExecutor(); - } - this.taskExecutor.execute(this.monitorRunnable); - } - this.monitorRunning = true; - } - } - - protected void stopMonitor() { - synchronized (this.monitorRunnable) { - if (this.monitorRunning) { - this.monitorRunnable.interrupt(); - } - this.monitorRunning = false; - } - } - - public void destroy() throws Exception { - this.stop(); - } - - - private class MonitorRunnable implements Runnable { - - private volatile Thread thread; - - private final FolderConnection folderConnection; - - - private MonitorRunnable(FolderConnection folderConnection) { - Assert.notNull(folderConnection, "folderConnection must not be null"); - this.folderConnection = folderConnection; - } - - - public synchronized void interrupt() { - if (this.thread != null) { - this.thread.interrupt(); - } - else if (logger.isInfoEnabled()) { - logger.info("monitor is not running, cannot interrupt"); - } - } - - public void run() { - this.thread = Thread.currentThread(); - while (!Thread.currentThread().isInterrupted()) { - if (!this.folderConnection.isRunning()) { - this.folderConnection.start(); - } - Message[] mailMessages = this.folderConnection.receive(); - for (Message mailMessage : mailMessages) { - ListeningMailSource.this.sendMessage(MessageBuilder.withPayload(mailMessage).build()); - } - } - } - } - -} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/FolderConnection.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/MailReceiver.java similarity index 69% rename from org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/FolderConnection.java rename to org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/MailReceiver.java index 3e2861ecff..88c10316a3 100644 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/FolderConnection.java +++ b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/MailReceiver.java @@ -16,19 +16,13 @@ package org.springframework.integration.mail; -import javax.mail.Folder; -import javax.mail.Message; - -import org.springframework.context.Lifecycle; - /** - * Encapsulates state for a restartable connection to a {@link Folder} and - * ensures thread safety. + * Strategy interface for receiving mail {@link javax.mail.Message Messages}. * - * @author Jonas Partner + * @author Mark Fisher */ -public interface FolderConnection extends Lifecycle { +public interface MailReceiver { - Message[] receive(); + javax.mail.Message[] receive() throws javax.mail.MessagingException; } diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/PollingMailSource.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/MailReceivingMessageSource.java similarity index 66% rename from org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/PollingMailSource.java rename to org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/MailReceivingMessageSource.java index 6d3cb0a2dc..a42d9c9d08 100644 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/PollingMailSource.java +++ b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/MailReceivingMessageSource.java @@ -16,6 +16,7 @@ package org.springframework.integration.mail; +import java.util.Arrays; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; @@ -24,50 +25,59 @@ import org.apache.commons.logging.LogFactory; import org.springframework.integration.core.Message; import org.springframework.integration.core.MessagingException; -import org.springframework.integration.mail.monitor.MonitoringStrategy; import org.springframework.integration.message.MessageBuilder; import org.springframework.integration.message.MessageSource; import org.springframework.util.Assert; /** * {@link MessageSource} implementation that delegates to a - * {@link MonitoringStrategy} to poll a mailbox. Each poll of the mailbox may + * {@link MailReceiver} to poll a mailbox. Each poll of the mailbox may * return more than one message which will then be stored in a queue. * - * @author Jonas Partner + * @author Jonas Partner * @author Mark Fisher */ -public class PollingMailSource implements MessageSource { +public class MailReceivingMessageSource implements MessageSource { private final Log logger = LogFactory.getLog(this.getClass()); - private final FolderConnection folderConnection; + private final MailReceiver mailReceiver; private final Queue mailQueue = new ConcurrentLinkedQueue(); - public PollingMailSource(FolderConnection folderConnection) { - Assert.notNull(folderConnection, "folderConnection must not be null"); - this.folderConnection = folderConnection; + public MailReceivingMessageSource(MailReceiver mailReceiver) { + Assert.notNull(mailReceiver, "mailReceiver must not be null"); + this.mailReceiver = mailReceiver; + } + + public MailReceivingMessageSource(String url) { + if (url.startsWith("imap")) { + this.mailReceiver = new Pop3MailReceiver(url); + } + else if (url.startsWith("pop3")) { + this.mailReceiver = new ImapMailReceiver(url); + } + else { + throw new UnsupportedOperationException("unsupported mail protocol '" + + url.substring(0, url.indexOf(':')) + "'"); + } } - @SuppressWarnings("unchecked") public Message receive() { try { javax.mail.Message mailMessage = this.mailQueue.poll(); if (mailMessage == null) { - javax.mail.Message[] messages = this.folderConnection.receive(); + javax.mail.Message[] messages = this.mailReceiver.receive(); if (messages != null) { - for (javax.mail.Message message : messages) { - this.mailQueue.add(message); - } + this.mailQueue.addAll(Arrays.asList(messages)); } mailMessage = this.mailQueue.poll(); } if (mailMessage != null) { if (logger.isDebugEnabled()) { - logger.debug("Received mail message [" + mailMessage + "]"); + logger.debug("received mail message [" + mailMessage + "]"); } return MessageBuilder.withPayload(mailMessage).build(); } diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/Pop3MailReceiver.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/Pop3MailReceiver.java new file mode 100755 index 0000000000..9f5f5df23e --- /dev/null +++ b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/Pop3MailReceiver.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2008 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.integration.mail; + +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.URLName; +import javax.mail.internet.MimeMessage; + +import org.springframework.integration.mail.monitor.MailTransportUtils; +import org.springframework.util.Assert; + +/** + * A {@link MailReceiver} implementation that polls a mail server using the + * POP3 protocol. + * + * @author Arjen Poutsma + * @author Mark Fisher + */ +public class Pop3MailReceiver extends AbstractMailReceiver { + + public Pop3MailReceiver(String url) { + super(url); + Assert.isTrue(url.startsWith("pop3"), "url must start with 'pop3'"); + } + + public Pop3MailReceiver(String host, String username, String password) { + // -1 indicates default port + this(host, -1, username, password); + } + + public Pop3MailReceiver(String host, int port, String username, String password) { + super(new URLName("pop3", host, port, "INBOX", username, password)); + } + + + /** + * POP3 is unable to detect new Messages, so this always returns true. + */ + @Override + protected final boolean shouldDeleteMessages() { + return true; + } + + @Override + protected Message[] searchForNewMessages() throws MessagingException { + int messageCount = this.getFolder().getMessageCount(); + if (messageCount == 0) { + return new Message[0]; + } + return this.getFolder().getMessages(); + } + + /** + * Deletes the given messages from this receiver's folder, and closes it to expunge deleted messages. + * + * @param messages the messages to delete + * @throws MessagingException in case of JavaMail errors + */ + @Override + protected void deleteMessages(Message[] messages) throws MessagingException { + super.deleteMessages(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(this.getFolder(), true); + } + +} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/SubscribableImapIdleMailSourceParser.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/ImapIdleChannelAdapterParser.java similarity index 64% rename from org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/SubscribableImapIdleMailSourceParser.java rename to org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/ImapIdleChannelAdapterParser.java index d54c15e198..8cec6707b9 100644 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/SubscribableImapIdleMailSourceParser.java +++ b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/ImapIdleChannelAdapterParser.java @@ -21,22 +21,20 @@ import org.w3c.dom.Element; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.integration.mail.DefaultFolderConnection; -import org.springframework.integration.mail.ListeningMailSource; -import org.springframework.integration.mail.monitor.ImapIdleMonitoringStrategy; +import org.springframework.integration.mail.ImapIdleChannelAdapter; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * Parser for the <imap-idle-mail-source> element in the 'mail' namespace. + * Parser for the <imap-idle-channel-adapter> element in the 'mail' namespace. * * @author Jonas Partner * @author Mark Fisher */ -public class SubscribableImapIdleMailSourceParser extends AbstractSingleBeanDefinitionParser { +public class ImapIdleChannelAdapterParser extends AbstractSingleBeanDefinitionParser { protected Class getBeanClass(Element element) { - return ListeningMailSource.class; + return ImapIdleChannelAdapter.class; } protected boolean shouldGenerateId() { @@ -51,24 +49,20 @@ public class SubscribableImapIdleMailSourceParser extends AbstractSingleBeanDefi String channel = element.getAttribute("channel"); String uri = element.getAttribute("store-uri"); String taskExecutorRef = element.getAttribute("task-executor"); - String propertiesRef = element.getAttribute("javaMailProperties"); + String propertiesRef = element.getAttribute("java-mail-properties"); Assert.hasText(channel, "the 'channel' attribute is required"); Assert.hasText(uri, "the 'store-uri' attribute is required"); - BeanDefinitionBuilder folderConnectionBuilder = - BeanDefinitionBuilder.genericBeanDefinition(DefaultFolderConnection.class); Assert.isTrue(uri.toLowerCase().startsWith("imap"), - "store-uri must start with 'imap' for the imap idle source"); - folderConnectionBuilder.addConstructorArgValue(uri); - folderConnectionBuilder.addConstructorArgValue(new ImapIdleMonitoringStrategy()); - // set polling false - folderConnectionBuilder.addConstructorArgValue(false); + "store-uri must start with 'imap' for the imap idle channel adapter"); + builder.addConstructorArgValue(uri); if (StringUtils.hasText(propertiesRef)) { - folderConnectionBuilder.addPropertyReference("javaMailProperties", propertiesRef); + builder.addPropertyReference("javaMailProperties", propertiesRef); } - builder.addConstructorArgValue(folderConnectionBuilder.getBeanDefinition()); if (StringUtils.hasLength(taskExecutorRef)) { builder.addPropertyReference("taskExecutor", taskExecutorRef); } + builder.addPropertyValue("shouldDeleteMessages", + !"false".equals(element.getAttribute("should-delete-messages"))); builder.addPropertyReference("outputChannel", channel); } diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/MailInboundChannelAdapterParser.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/MailInboundChannelAdapterParser.java new file mode 100644 index 0000000000..93a2a3e9f1 --- /dev/null +++ b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/MailInboundChannelAdapterParser.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2008 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.integration.mail.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.AbstractPollingInboundChannelAdapterParser; +import org.springframework.integration.mail.ImapMailReceiver; +import org.springframework.integration.mail.MailReceiver; +import org.springframework.integration.mail.MailReceivingMessageSource; +import org.springframework.integration.mail.Pop3MailReceiver; +import org.springframework.util.Assert; + +/** + * Parser for the <inbound-channel-adapter> element of Spring + * Integration's 'mail' namespace. + * + * @author Jonas Partner + * @author Mark Fisher + */ +public class MailInboundChannelAdapterParser extends AbstractPollingInboundChannelAdapterParser { + + @Override + protected String parseSource(Element element, ParserContext parserContext) { + String uri = element.getAttribute("store-uri"); + //String propertiesRef = element.getAttribute("javaMailProperties"); + Assert.hasText(uri, "the 'store-uri' attribute is required"); + boolean isPop3 = uri.toLowerCase().startsWith("pop3"); + boolean isImap = uri.toLowerCase().startsWith("imap"); + Assert.isTrue(isPop3 || isImap, "the 'store-uri' must begin with 'pop3' or 'imap'"); + MailReceiver mailReceiver = isPop3 ? new Pop3MailReceiver(uri) : new ImapMailReceiver(uri); + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MailReceivingMessageSource.class); + /* + if (StringUtils.hasText(propertiesRef)) { + folderConnectionBuilder.addPropertyReference("javaMailProperties", + propertiesRef); + } + */ + builder.addConstructorArgValue(mailReceiver); + return BeanDefinitionReaderUtils.registerWithGeneratedName( + builder.getBeanDefinition(), parserContext.getRegistry()); + } + +} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/MailNamespaceHandler.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/MailNamespaceHandler.java index ef9ebdbafe..68db456faa 100644 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/MailNamespaceHandler.java +++ b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/MailNamespaceHandler.java @@ -29,8 +29,8 @@ public class MailNamespaceHandler extends NamespaceHandlerSupport { public void init() { this.registerBeanDefinitionParser("outbound-channel-adapter", new MailOutboundChannelAdapterParser()); - this.registerBeanDefinitionParser("polling-mail-source", new PollingMailSourceParser()); - this.registerBeanDefinitionParser("imap-idle-mail-source", new SubscribableImapIdleMailSourceParser()); + this.registerBeanDefinitionParser("inbound-channel-adapter", new MailInboundChannelAdapterParser()); + this.registerBeanDefinitionParser("imap-idle-channel-adapter", new ImapIdleChannelAdapterParser()); this.registerBeanDefinitionParser("header-enricher", new SimpleHeaderEnricherParser(MailHeaders.PREFIX)); this.registerBeanDefinitionParser("mail-to-string-transformer", new MailToStringTransformerParser()); } diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/PollingMailSourceParser.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/PollingMailSourceParser.java deleted file mode 100644 index 191a9f9dd1..0000000000 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/PollingMailSourceParser.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2002-2008 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.integration.mail.config; - -import org.w3c.dom.Element; - -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; -import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.integration.mail.DefaultFolderConnection; -import org.springframework.integration.mail.PollingMailSource; -import org.springframework.integration.mail.monitor.MonitoringStrategy; -import org.springframework.integration.mail.monitor.PollingMonitoringStrategy; -import org.springframework.integration.mail.monitor.Pop3PollingMonitoringStrategy; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * @author Jonas Partner - */ -public class PollingMailSourceParser extends AbstractSingleBeanDefinitionParser { - - protected Class getBeanClass(Element element) { - return PollingMailSource.class; - } - - protected boolean shouldGenerateId() { - return false; - } - - protected boolean shouldGenerateIdAsFallback() { - return true; - } - - protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { - String mailConvertorRef = element.getAttribute("convertor"); - String uri = element.getAttribute("store-uri"); - String propertiesRef = element.getAttribute("javaMailProperties"); - Assert.hasText(uri, "the 'store-uri' attribute is required"); - BeanDefinitionBuilder folderConnectionBuilder = BeanDefinitionBuilder - .genericBeanDefinition(DefaultFolderConnection.class); - MonitoringStrategy monitoringStrategy = null; - if (uri.toLowerCase().startsWith("pop3")) { - monitoringStrategy = new Pop3PollingMonitoringStrategy(); - } - else if (uri.toLowerCase().startsWith("imap")) { - monitoringStrategy = new PollingMonitoringStrategy(); - } - else { - throw new IllegalArgumentException( - "unable to determine monitoring strategy for store-uri [" + uri + "]"); - } - folderConnectionBuilder.addConstructorArgValue(uri); - folderConnectionBuilder.addConstructorArgValue(monitoringStrategy); - // set polling true - folderConnectionBuilder.addConstructorArgValue(true); - - if (StringUtils.hasText(propertiesRef)) { - folderConnectionBuilder.addPropertyReference("javaMailProperties", - propertiesRef); - } - String folderConnectionName = parserContext.getReaderContext() - .registerWithGeneratedName(folderConnectionBuilder.getBeanDefinition()); - builder.addDependsOn(folderConnectionName); - builder.addConstructorArgValue(folderConnectionBuilder.getBeanDefinition()); - if (StringUtils.hasText(mailConvertorRef)) { - builder.addPropertyReference("convertor", mailConvertorRef); - } - } - -} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/spring-integration-mail-1.0.xsd b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/spring-integration-mail-1.0.xsd index 56fe412c89..55ae9c472d 100644 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/spring-integration-mail-1.0.xsd +++ b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/config/spring-integration-mail-1.0.xsd @@ -3,12 +3,14 @@ xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:tool="http://www.springframework.org/schema/tool" + xmlns:integration="http://www.springframework.org/schema/integration" targetNamespace="http://www.springframework.org/schema/integration/mail" elementFormDefault="qualified" attributeFormDefault="unqualified"> + - + - Defines a polling mail source. + Defines an inbound Channel Adapter that polls a mailbox for mail messages. - + + + + + - + - Defines an IMAP mail source. + Defines an IMAP IDLE channel adapter. - - + + - + + + diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/AbstractMonitoringStrategy.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/AbstractMonitoringStrategy.java deleted file mode 100644 index 2db320a44b..0000000000 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/AbstractMonitoringStrategy.java +++ /dev/null @@ -1,169 +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.integration.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 - */ -public abstract class AbstractMonitoringStrategy implements MonitoringStrategy { - - /** Logger available to subclasses. */ - protected final Log logger = LogFactory.getLog(getClass()); - - private boolean deleteMessages = true; - - private int maxMessagesPerReceive = -1; - - /** - * Sets whether messages should be marked as {@link javax.mail.Flags.Flag#DELETED DELETED} after they have been - * read. Default is true. - */ - public void setDeleteMessages(boolean deleteMessages) { - this.deleteMessages = deleteMessages; - } - - public int getFolderOpenMode() { - return deleteMessages ? Folder.READ_WRITE : Folder.READ_ONLY; - } - - public void setMaxMessagePerDownload(int maxMessagesPerReceive){ - this.maxMessagesPerReceive = maxMessagesPerReceive; - } - - /** - * 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 true. - * - * @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[] receive(Folder folder) throws MessagingException, InterruptedException { - logger.info("Receiving for folder" + folder.getFullName()); - if(!folder.isOpen()){ - folder.open(getFolderOpenMode()); - } - folder.getMessageCount(); - Message[] messages = searchForNewMessages(folder); - if (logger.isDebugEnabled()) { - logger.debug("Found " + messages.length + " new messages"); - } - if(maxMessagesPerReceive > 0 && messages.length > maxMessagesPerReceive){ - Message[] reducedMessages = new Message[maxMessagesPerReceive]; - System.arraycopy(messages, 0, reducedMessages, 0, maxMessagesPerReceive); - messages = reducedMessages; - } - - if (messages.length > 0) { - fetchMessages(folder, messages); - } - if (deleteMessages) { - deleteMessages(folder, messages); - } - return messages; - } - - /** - * Retrieves new messages from the given folder. This implementation creates a {@link SearchTerm} that searches for - * all messages in the folder that are {@link javax.mail.Flags.Flag#RECENT RECENT}, not {@link - * javax.mail.Flags.Flag#ANSWERED ANSWERED}, and not {@link javax.mail.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 != 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 javax.mail.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 - * true. - * - * @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); - } - } -} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/AsyncMonitoringStrategy.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/AsyncMonitoringStrategy.java deleted file mode 100644 index 25efbd4c28..0000000000 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/AsyncMonitoringStrategy.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2002-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.integration.mail.monitor; - -import javax.mail.Folder; -import javax.mail.MessagingException; - -public interface AsyncMonitoringStrategy { - - public abstract void waitForNewMessages(Folder folder) - throws MessagingException, InterruptedException; - -} \ No newline at end of file diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/ImapIdleMonitoringStrategy.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/ImapIdleMonitoringStrategy.java deleted file mode 100644 index e75cf15074..0000000000 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/ImapIdleMonitoringStrategy.java +++ /dev/null @@ -1,81 +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.integration.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. - *

- * Note 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 - */ -public class ImapIdleMonitoringStrategy extends AbstractMonitoringStrategy implements AsyncMonitoringStrategy { - - private MessageCountListener messageCountListener; - - /* (non-Javadoc) - * @see org.springframework.integration.adapter.mail.monitor.AynchronouseMonitoringStrategy#waitForNewMessages(javax.mail.Folder) - */ - public 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 - } - } - } - }; - } -} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/MailTransportConstants.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/MailTransportConstants.java deleted file mode 100644 index d3b05d40e3..0000000000 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/MailTransportConstants.java +++ /dev/null @@ -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.integration.mail.monitor; - - -/** - * Declares Mail-specific transport constants. - * - * @author Arjen Poutsma - */ -public interface MailTransportConstants { - - /** - * The "mail" URI scheme. - */ - String MAIL_URI_SCHEME = "mailto"; - - /** - * The "In-Reply-To" header. - */ - String HEADER_IN_REPLY_TO = "In-Reply-To"; -} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/MailTransportUtils.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/MailTransportUtils.java index fff6241bd1..13f6b11eda 100644 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/MailTransportUtils.java +++ b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/MailTransportUtils.java @@ -17,7 +17,6 @@ package org.springframework.integration.mail.monitor; import java.net.URI; -import java.net.URISyntaxException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -167,21 +166,4 @@ public abstract class MailTransportUtils { return tempURL.toString(); } - /** - * Converts the given internet address into a mailto URI. - * - * @param to the To: address - * @param subject the subject, may be null - * @return a mailto URI - */ - public static URI toUri(InternetAddress to, String subject) throws URISyntaxException { - if (StringUtils.hasLength(subject)) { - return new URI(MailTransportConstants.MAIL_URI_SCHEME, to.getAddress() + "?subject=" + subject, null); - } - else { - return new URI(MailTransportConstants.MAIL_URI_SCHEME, to.getAddress(), null); - } - } - - } diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/MonitoringStrategy.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/MonitoringStrategy.java deleted file mode 100644 index f75e42c1a1..0000000000 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/MonitoringStrategy.java +++ /dev/null @@ -1,47 +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.integration.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 IDLE 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[] receive(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(); - -} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/PollingMonitoringStrategy.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/PollingMonitoringStrategy.java deleted file mode 100644 index 1add4f4f60..0000000000 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/PollingMonitoringStrategy.java +++ /dev/null @@ -1,33 +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.integration.mail.monitor; - - -/** - * 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. - *

- * Note that this implementation is not suitable for use with POP3 servers. Use the {@link - * Pop3PollingMonitoringStrategy} instead. - * - * @author Arjen Poutsma - */ -public class PollingMonitoringStrategy extends AbstractMonitoringStrategy { - - - -} diff --git a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/Pop3PollingMonitoringStrategy.java b/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/Pop3PollingMonitoringStrategy.java deleted file mode 100644 index a38f297605..0000000000 --- a/org.springframework.integration.mail/src/main/java/org/springframework/integration/mail/monitor/Pop3PollingMonitoringStrategy.java +++ /dev/null @@ -1,66 +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.integration.mail.monitor; - -import javax.mail.Folder; -import javax.mail.Message; -import javax.mail.MessagingException; -import javax.mail.internet.MimeMessage; - -/** - * 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. - *

- * Setting the {@link #setDeleteMessages(boolean) deleteMessages} property is therefore ignored: messages are always - * deleted. - * - * @author Arjen Poutsma - */ -public class Pop3PollingMonitoringStrategy extends PollingMonitoringStrategy { - - public Pop3PollingMonitoringStrategy() { - super.setDeleteMessages(true); - } - - public void setDeleteMessages(boolean deleteMessages) { - } - - /** - * 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); - } -} \ No newline at end of file diff --git a/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/PollingMessageSourceTests.java b/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/MailReceivingMessageSourceTests.java similarity index 61% rename from org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/PollingMessageSourceTests.java rename to org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/MailReceivingMessageSourceTests.java index c57fc1311c..8f570ddf8e 100644 --- a/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/PollingMessageSourceTests.java +++ b/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/MailReceivingMessageSourceTests.java @@ -28,31 +28,32 @@ import org.junit.Test; /** * @author Jonas Partner + * @author Mark Fisher */ -public class PollingMessageSourceTests { +public class MailReceivingMessageSourceTests { @Test public void testPolling() { - StubFolderConnection folderConnection = new StubFolderConnection(); + StubMailReceiver mailReceiver = new StubMailReceiver(); MimeMessage message1 = EasyMock.createMock(MimeMessage.class); MimeMessage message2 = EasyMock.createMock(MimeMessage.class); MimeMessage message3 = EasyMock.createMock(MimeMessage.class); MimeMessage message4 = EasyMock.createMock(MimeMessage.class); - folderConnection.messages.add(new javax.mail.Message[] { message1 }); - folderConnection.messages.add(new javax.mail.Message[] { message2, message3 }); - folderConnection.messages.add(new javax.mail.Message[] { message4 }); + mailReceiver.messages.add(new javax.mail.Message[] { message1 }); + mailReceiver.messages.add(new javax.mail.Message[] { message2, message3 }); + mailReceiver.messages.add(new javax.mail.Message[] { message4 }); - PollingMailSource pollingMailSource = new PollingMailSource(folderConnection); - assertEquals("Wrong message for number 1", message1, pollingMailSource.receive().getPayload()); - assertEquals("Wrong message for number 2", message2, pollingMailSource.receive().getPayload()); - assertEquals("Wrong message for number 3", message3, pollingMailSource.receive().getPayload()); - assertEquals("Wrong message for number 4", message4, pollingMailSource.receive().getPayload()); - assertNull("Expected null after exhausting all messages", pollingMailSource.receive()); + MailReceivingMessageSource source = new MailReceivingMessageSource(mailReceiver); + assertEquals("Wrong message for number 1", message1, source.receive().getPayload()); + assertEquals("Wrong message for number 2", message2, source.receive().getPayload()); + assertEquals("Wrong message for number 3", message3, source.receive().getPayload()); + assertEquals("Wrong message for number 4", message4, source.receive().getPayload()); + assertNull("Expected null after exhausting all messages", source.receive()); } - private static class StubFolderConnection implements FolderConnection { + private static class StubMailReceiver implements MailReceiver { private final ConcurrentLinkedQueue messages = new ConcurrentLinkedQueue(); diff --git a/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/SubscribableMailSourceTests.java b/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/SubscribableMailSourceTests.java deleted file mode 100644 index f8af56660b..0000000000 --- a/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/SubscribableMailSourceTests.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2002-2008 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.integration.mail; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import java.util.concurrent.ConcurrentLinkedQueue; - -import javax.mail.internet.MimeMessage; - -import org.easymock.classextension.EasyMock; -import org.junit.Test; - -import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.core.Message; - -/** - * @author Jonas Partner - */ -public class SubscribableMailSourceTests { - - @Test - public void testReceive() throws Exception { - javax.mail.Message message = EasyMock.createMock(MimeMessage.class); - StubFolderConnection folderConnection = new StubFolderConnection(message); - QueueChannel channel = new QueueChannel(); - ListeningMailSource mailSource = new ListeningMailSource(folderConnection); - mailSource.setOutputChannel(channel); - mailSource.start(); - Message result = channel.receive(1000); - mailSource.stop(); - assertNotNull(result); - assertEquals("Wrong payload", message, result.getPayload()); - mailSource.stop(); - } - - - private static class StubFolderConnection implements FolderConnection { - - private final ConcurrentLinkedQueue messages = new ConcurrentLinkedQueue(); - - public StubFolderConnection(javax.mail.Message message) { - messages.add(message); - } - - public javax.mail.Message[] receive() { - javax.mail.Message msg = messages.poll(); - if (msg == null) { - return new javax.mail.Message[] {}; - } - return new javax.mail.Message[] { msg }; - } - - public boolean isRunning() { - return false; - } - - public void start() { - } - - public void stop() { - } - } - -} diff --git a/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/config/PollingMailSourceParserTests.java b/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/config/PollingMailSourceParserTests.java index 26fff9bbb3..b2780a1e77 100644 --- a/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/config/PollingMailSourceParserTests.java +++ b/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/config/PollingMailSourceParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-2008 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. @@ -13,19 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.integration.mail.config; import org.junit.Test; + import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.integration.mail.PollingMailSource; +/** + * @author Jonas Partner + */ public class PollingMailSourceParserTests { - + @Test public void testPop3(){ ApplicationContext context = new ClassPathXmlApplicationContext("pollingMailSourceParserTests.xml", PollingMailSourceParserTests.class); - PollingMailSource mailSource = (PollingMailSource)context.getBean("pollingPop3"); + context.getBean("pollingPop3"); } } diff --git a/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/config/mailOutboundChannelAdapterParserTests.xml b/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/config/mailOutboundChannelAdapterParserTests.xml index e891b930f7..d3fdb8cf70 100644 --- a/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/config/mailOutboundChannelAdapterParserTests.xml +++ b/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/config/mailOutboundChannelAdapterParserTests.xml @@ -2,8 +2,11 @@ diff --git a/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/config/pollingMailSourceParserTests.xml b/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/config/pollingMailSourceParserTests.xml index 61660b74a8..0b96d28d40 100644 --- a/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/config/pollingMailSourceParserTests.xml +++ b/org.springframework.integration.mail/src/test/java/org/springframework/integration/mail/config/pollingMailSourceParserTests.xml @@ -2,11 +2,14 @@ - +