/*
* Copyright 2002-2017 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.Date;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import javax.mail.FolderClosedException;
import javax.mail.Message;
import javax.mail.MessagingException;
import org.aopalliance.aop.Advice;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.integration.mail.event.MailIntegrationEvent;
import org.springframework.integration.transaction.IntegrationResourceHolder;
import org.springframework.integration.transaction.IntegrationResourceHolderSynchronization;
import org.springframework.integration.transaction.TransactionSynchronizationFactory;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* 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
* @author Oleg Zhurakousky
* @author Gary Russell
* @author Artem Bilan
*/
public class ImapIdleChannelAdapter extends MessageProducerSupport implements BeanClassLoaderAware,
ApplicationEventPublisherAware {
private static final int DEFAULT_RECONNECT_DELAY = 10000;
private final IdleTask idleTask = new IdleTask();
private volatile Executor sendingTaskExecutor;
private volatile boolean sendingTaskExecutorSet;
private volatile boolean shouldReconnectAutomatically = true;
private volatile ClassLoader classLoader;
private volatile List<Advice> adviceChain;
private final ImapMailReceiver mailReceiver;
private volatile long reconnectDelay = DEFAULT_RECONNECT_DELAY; // milliseconds
private volatile ScheduledFuture<?> receivingTask;
private final ExceptionAwarePeriodicTrigger receivingTaskTrigger = new ExceptionAwarePeriodicTrigger();
private volatile TransactionSynchronizationFactory transactionSynchronizationFactory;
private volatile ApplicationEventPublisher applicationEventPublisher;
public ImapIdleChannelAdapter(ImapMailReceiver mailReceiver) {
Assert.notNull(mailReceiver, "'mailReceiver' must not be null");
this.mailReceiver = mailReceiver;
}
public void setTransactionSynchronizationFactory(
TransactionSynchronizationFactory transactionSynchronizationFactory) {
this.transactionSynchronizationFactory = transactionSynchronizationFactory;
}
public void setAdviceChain(List<Advice> adviceChain) {
this.adviceChain = adviceChain;
}
/**
* Specify an {@link Executor} used to send messages received by the
* adapter.
* @param sendingTaskExecutor the sendingTaskExecutor to set
*/
public void setSendingTaskExecutor(Executor sendingTaskExecutor) {
Assert.notNull(sendingTaskExecutor, "'sendingTaskExecutor' must not be null");
this.sendingTaskExecutor = sendingTaskExecutor;
this.sendingTaskExecutorSet = true;
}
/**
* Specify whether the IDLE task should reconnect automatically after
* catching a {@link FolderClosedException} while waiting for messages. The
* default value is <code>true</code>.
* @param shouldReconnectAutomatically true to reconnect.
*/
public void setShouldReconnectAutomatically(boolean shouldReconnectAutomatically) {
this.shouldReconnectAutomatically = shouldReconnectAutomatically;
}
/**
* The time between connection attempts in milliseconds (default 10 seconds).
* @param reconnectDelay the reconnectDelay to set
* @since 3.0.5
*/
public void setReconnectDelay(long reconnectDelay) {
this.reconnectDelay = reconnectDelay;
}
@Override
public String getComponentType() {
return "mail:imap-idle-channel-adapter";
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/*
* Lifecycle implementation
*/
@Override // guarded by super#lifecycleLock
protected void doStart() {
final TaskScheduler scheduler = this.getTaskScheduler();
Assert.notNull(scheduler, "'taskScheduler' must not be null");
if (this.sendingTaskExecutor == null) {
this.sendingTaskExecutor = Executors.newFixedThreadPool(1);
}
this.receivingTask = scheduler.schedule(new ReceivingTask(), this.receivingTaskTrigger);
}
@Override
// guarded by super#lifecycleLock
protected void doStop() {
this.receivingTask.cancel(true);
try {
this.mailReceiver.destroy();
}
catch (Exception e) {
throw new IllegalStateException(
"Failure during the destruction of Mail receiver: " + this.mailReceiver, e);
}
/*
* If we're running with the default executor, shut it down.
*/
if (!this.sendingTaskExecutorSet && this.sendingTaskExecutor != null) {
((ExecutorService) this.sendingTaskExecutor).shutdown();
this.sendingTaskExecutor = null;
}
}
private Runnable createMessageSendingTask(final Object mailMessage) {
Runnable sendingTask = () -> {
@SuppressWarnings("unchecked")
org.springframework.messaging.Message<?> message =
mailMessage instanceof Message
? ImapIdleChannelAdapter.this.getMessageBuilderFactory().withPayload(mailMessage).build()
: (org.springframework.messaging.Message<Object>) mailMessage;
if (TransactionSynchronizationManager.isActualTransactionActive()) {
if (ImapIdleChannelAdapter.this.transactionSynchronizationFactory != null) {
TransactionSynchronization synchronization =
ImapIdleChannelAdapter.this.transactionSynchronizationFactory
.create(ImapIdleChannelAdapter.this);
if (synchronization != null) {
TransactionSynchronizationManager.registerSynchronization(synchronization);
if (synchronization instanceof IntegrationResourceHolderSynchronization
&& !TransactionSynchronizationManager.hasResource(ImapIdleChannelAdapter.this)) {
TransactionSynchronizationManager.bindResource(ImapIdleChannelAdapter.this,
((IntegrationResourceHolderSynchronization) synchronization).getResourceHolder());
}
Object resourceHolder =
TransactionSynchronizationManager.getResource(ImapIdleChannelAdapter.this);
if (resourceHolder instanceof IntegrationResourceHolder) {
((IntegrationResourceHolder) resourceHolder).setMessage(message);
}
}
}
}
sendMessage(message);
};
// wrap in the TX proxy if necessary
if (!CollectionUtils.isEmpty(this.adviceChain)) {
ProxyFactory proxyFactory = new ProxyFactory(sendingTask);
if (!CollectionUtils.isEmpty(this.adviceChain)) {
for (Advice advice : this.adviceChain) {
proxyFactory.addAdvice(advice);
}
}
sendingTask = (Runnable) proxyFactory.getProxy(this.classLoader);
}
return sendingTask;
}
private void publishException(Exception e) {
if (this.applicationEventPublisher != null) {
this.applicationEventPublisher.publishEvent(new ImapIdleExceptionEvent(e));
}
else {
if (logger.isDebugEnabled()) {
logger.debug("No application event publisher for exception: " + e.getMessage());
}
}
}
private class ReceivingTask implements Runnable {
ReceivingTask() {
super();
}
@Override
public void run() {
try {
ImapIdleChannelAdapter.this.idleTask.run();
if (logger.isDebugEnabled()) {
logger.debug("Task completed successfully. Re-scheduling it again right away.");
}
}
catch (Exception e) { //run again after a delay
logger.warn("Failed to execute IDLE task. Will attempt to resubmit in " + ImapIdleChannelAdapter.this.reconnectDelay + " milliseconds.", e);
ImapIdleChannelAdapter.this.receivingTaskTrigger.delayNextExecution();
ImapIdleChannelAdapter.this.publishException(e);
}
}
}
private class IdleTask implements Runnable {
IdleTask() {
super();
}
@Override
public void run() {
final TaskScheduler scheduler = getTaskScheduler();
Assert.notNull(scheduler, "'taskScheduler' must not be null");
/*
* The following shouldn't be necessary because doStart() will have ensured we have
* one. But, just in case...
*/
Assert.state(ImapIdleChannelAdapter.this.sendingTaskExecutor != null, "'sendingTaskExecutor' must not be null");
try {
if (logger.isDebugEnabled()) {
logger.debug("waiting for mail");
}
ImapIdleChannelAdapter.this.mailReceiver.waitForNewMessages();
if (ImapIdleChannelAdapter.this.mailReceiver.getFolder().isOpen()) {
Object[] mailMessages = ImapIdleChannelAdapter.this.mailReceiver.receive();
if (logger.isDebugEnabled()) {
logger.debug("received " + mailMessages.length + " mail messages");
}
for (final Object mailMessage : mailMessages) {
Runnable messageSendingTask = createMessageSendingTask(mailMessage);
ImapIdleChannelAdapter.this.sendingTaskExecutor.execute(messageSendingTask);
}
}
}
catch (MessagingException e) {
if (logger.isWarnEnabled()) {
logger.warn("error occurred in idle task", e);
}
if (ImapIdleChannelAdapter.this.shouldReconnectAutomatically) {
throw new IllegalStateException(
"Failure in 'idle' task. Will resubmit.", e);
}
else {
throw new org.springframework.messaging.MessagingException(
"Failure in 'idle' task. Will NOT resubmit.", e);
}
}
}
}
private class ExceptionAwarePeriodicTrigger implements Trigger {
private volatile boolean delayNextExecution;
ExceptionAwarePeriodicTrigger() {
super();
}
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
if (this.delayNextExecution) {
this.delayNextExecution = false;
return new Date(System.currentTimeMillis() + ImapIdleChannelAdapter.this.reconnectDelay);
}
else {
return new Date(System.currentTimeMillis());
}
}
public void delayNextExecution() {
this.delayNextExecution = true;
}
}
public class ImapIdleExceptionEvent extends MailIntegrationEvent {
private static final long serialVersionUID = -5875388810251967741L;
public ImapIdleExceptionEvent(Exception e) {
super(ImapIdleChannelAdapter.this, e);
}
}
}