Skip to content

Commit 5295a76

Browse files
artembilangaryrussell
authored andcommitted
Fix mail lock race condition & Sonar smells (#2721)
* Fix mail lock race condition & Sonar smells https://build.spring.io/browse/INT-MASTERSPRING40-599/ * The `folderReadLock` might not be re-locked when `openFolder()` throws an exception * Fix all the Sonar smells for mail module * Optimize all the dynamic `Assert` messages to `Supplier` * Refactor `MailReceiverFactoryBean` to be based on the `AbstractFactoryBean` * * Fix new reported Sonar smells * * Log error during cancel idle state in the `ImapMailReceiver` * Remove commented code in the `MailTransportUtils`
1 parent a609bd9 commit 5295a76

10 files changed

+211
-221
lines changed

spring-integration-mail/src/main/java/org/springframework/integration/mail/AbstractMailReceiver.java

+7-11
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import java.util.Map;
2626
import java.util.Properties;
2727
import java.util.concurrent.locks.Lock;
28-
import java.util.concurrent.locks.ReadWriteLock;
2928
import java.util.concurrent.locks.ReentrantReadWriteLock;
3029

3130
import javax.mail.Authenticator;
@@ -72,7 +71,7 @@ public abstract class AbstractMailReceiver extends IntegrationObjectSupport impl
7271

7372
private final URLName url;
7473

75-
private final ReadWriteLock folderLock = new ReentrantReadWriteLock();
74+
private final ReentrantReadWriteLock folderLock = new ReentrantReadWriteLock();
7675

7776
private final Lock folderReadLock = this.folderLock.readLock();
7877

@@ -340,7 +339,7 @@ private Folder obtainFolderInstance() throws MessagingException {
340339

341340
@Override
342341
public Object[] receive() throws javax.mail.MessagingException {
343-
this.folderReadLock.lock();
342+
this.folderReadLock.lock(); // NOSONAR - guarded with the getReadHoldCount()
344343
try {
345344
try {
346345
Folder folderToCheck = getFolder();
@@ -362,7 +361,9 @@ public Object[] receive() throws javax.mail.MessagingException {
362361
}
363362
}
364363
finally {
365-
this.folderReadLock.unlock();
364+
if (this.folderLock.getReadHoldCount() > 0) {
365+
this.folderReadLock.unlock();
366+
}
366367
}
367368
}
368369

@@ -416,22 +417,17 @@ private Object[] convertMessagesIfNecessary(MimeMessage[] filteredMessages) {
416417
private Object extractContent(MimeMessage message, Map<String, Object> headers) {
417418
Object content;
418419
try {
419-
MimeMessage theMessage;
420+
MimeMessage theMessage = message;
420421
if (this.simpleContent) {
421422
theMessage = new IntegrationMimeMessage(message);
422423
}
423-
else {
424-
theMessage = message;
425-
}
426424
content = theMessage.getContent();
427425
if (content instanceof String) {
426+
headers.put(MessageHeaders.CONTENT_TYPE, "text/plain");
428427
String mailContentType = (String) headers.get(MailHeaders.CONTENT_TYPE);
429428
if (mailContentType != null && mailContentType.toLowerCase().startsWith("text")) {
430429
headers.put(MessageHeaders.CONTENT_TYPE, mailContentType);
431430
}
432-
else {
433-
headers.put(MessageHeaders.CONTENT_TYPE, "text/plain");
434-
}
435431
}
436432
else if (content instanceof InputStream) {
437433
ByteArrayOutputStream baos = new ByteArrayOutputStream();

spring-integration-mail/src/main/java/org/springframework/integration/mail/ImapIdleChannelAdapter.java

+37-35
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -154,7 +154,7 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv
154154

155155
@Override // guarded by super#lifecycleLock
156156
protected void doStart() {
157-
final TaskScheduler scheduler = this.getTaskScheduler();
157+
TaskScheduler scheduler = getTaskScheduler();
158158
Assert.notNull(scheduler, "'taskScheduler' must not be null");
159159
if (this.sendingTaskExecutor == null) {
160160
this.sendingTaskExecutor = Executors.newFixedThreadPool(1);
@@ -182,39 +182,8 @@ protected void doStop() {
182182
}
183183
}
184184

185-
private Runnable createMessageSendingTask(final Object mailMessage) {
186-
Runnable sendingTask = () -> {
187-
@SuppressWarnings("unchecked")
188-
org.springframework.messaging.Message<?> message =
189-
mailMessage instanceof Message
190-
? getMessageBuilderFactory().withPayload(mailMessage).build()
191-
: (org.springframework.messaging.Message<Object>) mailMessage;
192-
193-
if (TransactionSynchronizationManager.isActualTransactionActive()) {
194-
if (ImapIdleChannelAdapter.this.transactionSynchronizationFactory != null) {
195-
TransactionSynchronization synchronization =
196-
ImapIdleChannelAdapter.this.transactionSynchronizationFactory
197-
.create(ImapIdleChannelAdapter.this);
198-
if (synchronization != null) {
199-
TransactionSynchronizationManager.registerSynchronization(synchronization);
200-
201-
if (synchronization instanceof IntegrationResourceHolderSynchronization
202-
&& !TransactionSynchronizationManager.hasResource(ImapIdleChannelAdapter.this)) {
203-
204-
TransactionSynchronizationManager.bindResource(ImapIdleChannelAdapter.this,
205-
((IntegrationResourceHolderSynchronization) synchronization).getResourceHolder());
206-
}
207-
208-
Object resourceHolder =
209-
TransactionSynchronizationManager.getResource(ImapIdleChannelAdapter.this);
210-
if (resourceHolder instanceof IntegrationResourceHolder) {
211-
((IntegrationResourceHolder) resourceHolder).setMessage(message);
212-
}
213-
}
214-
}
215-
}
216-
sendMessage(message);
217-
};
185+
private Runnable createMessageSendingTask(Object mailMessage) {
186+
Runnable sendingTask = prepareSendingTask(mailMessage);
218187

219188
// wrap in the TX proxy if necessary
220189
if (!CollectionUtils.isEmpty(this.adviceChain)) {
@@ -229,6 +198,38 @@ private Runnable createMessageSendingTask(final Object mailMessage) {
229198
return sendingTask;
230199
}
231200

201+
private Runnable prepareSendingTask(Object mailMessage) {
202+
return () -> {
203+
@SuppressWarnings("unchecked")
204+
org.springframework.messaging.Message<?> message =
205+
mailMessage instanceof Message
206+
? getMessageBuilderFactory().withPayload(mailMessage).build()
207+
: (org.springframework.messaging.Message<Object>) mailMessage;
208+
209+
if (TransactionSynchronizationManager.isActualTransactionActive()
210+
&& this.transactionSynchronizationFactory != null) {
211+
212+
TransactionSynchronization synchronization = this.transactionSynchronizationFactory.create(this);
213+
if (synchronization != null) {
214+
TransactionSynchronizationManager.registerSynchronization(synchronization);
215+
216+
if (synchronization instanceof IntegrationResourceHolderSynchronization
217+
&& !TransactionSynchronizationManager.hasResource(this)) {
218+
219+
TransactionSynchronizationManager.bindResource(this,
220+
((IntegrationResourceHolderSynchronization) synchronization).getResourceHolder());
221+
}
222+
223+
Object resourceHolder = TransactionSynchronizationManager.getResource(this);
224+
if (resourceHolder instanceof IntegrationResourceHolder) {
225+
((IntegrationResourceHolder) resourceHolder).setMessage(message);
226+
}
227+
}
228+
}
229+
sendMessage(message);
230+
};
231+
}
232+
232233
private void publishException(Exception e) {
233234
if (this.applicationEventPublisher != null) {
234235
this.applicationEventPublisher.publishEvent(new ImapIdleExceptionEvent(e));
@@ -262,6 +263,7 @@ public void run() {
262263
publishException(e);
263264
}
264265
}
266+
265267
}
266268

267269

spring-integration-mail/src/main/java/org/springframework/integration/mail/ImapMailReceiver.java

+47-35
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public class ImapMailReceiver extends AbstractMailReceiver {
5959

6060
private static final int DEFAULT_CANCEL_IDLE_INTERVAL = 120000;
6161

62+
private static final String PROTOCOL = "imap";
63+
6264
private final MessageCountListener messageCountListener = new SimpleMessageCountListener();
6365

6466
private final IdleCanceler idleCanceler = new IdleCanceler();
@@ -77,17 +79,17 @@ public class ImapMailReceiver extends AbstractMailReceiver {
7779

7880
public ImapMailReceiver() {
7981
super();
80-
this.setProtocol("imap");
82+
setProtocol(PROTOCOL);
8183
}
8284

8385
public ImapMailReceiver(String url) {
8486
super(url);
8587
if (url != null) {
86-
Assert.isTrue(url.toLowerCase().startsWith("imap"),
88+
Assert.isTrue(url.toLowerCase().startsWith(PROTOCOL),
8789
"URL must start with 'imap' for the IMAP Mail receiver.");
8890
}
8991
else {
90-
this.setProtocol("imap");
92+
setProtocol(PROTOCOL);
9193
}
9294
}
9395

@@ -126,7 +128,7 @@ public void setShouldMarkMessagesAsRead(Boolean shouldMarkMessagesAsRead) {
126128
* @since 3.0.5
127129
*/
128130
public void setCancelIdleInterval(long cancelIdleInterval) {
129-
this.cancelIdleInterval = cancelIdleInterval * 1000;
131+
this.cancelIdleInterval = cancelIdleInterval * 1000; // NOSONAR - convert seconds to milliseconds
130132
}
131133

132134
@Override
@@ -140,7 +142,7 @@ protected void onInit() {
140142
this.isInternalScheduler = true;
141143
}
142144
Properties javaMailProperties = getJavaMailProperties();
143-
for (String name : new String[]{"imap", "imaps"}) {
145+
for (String name : new String[] { PROTOCOL, "imaps" }) {
144146
String peek = "mail." + name + ".peek";
145147
if (javaMailProperties.getProperty(peek) == null) {
146148
javaMailProperties.setProperty(peek, "true");
@@ -154,6 +156,9 @@ public void destroy() {
154156
if (this.isInternalScheduler) {
155157
((ThreadPoolTaskScheduler) this.scheduler).shutdown();
156158
}
159+
if (this.pingTask != null) {
160+
this.pingTask.cancel(true);
161+
}
157162
}
158163

159164
/**
@@ -162,18 +167,16 @@ public void destroy() {
162167
* @throws MessagingException Any MessagingException.
163168
*/
164169
public void waitForNewMessages() throws MessagingException {
165-
this.openFolder();
166-
Folder folder = this.getFolder();
170+
openFolder();
171+
Folder folder = getFolder();
167172
Assert.state(folder instanceof IMAPFolder,
168-
"folder is not an instance of [" + IMAPFolder.class.getName() + "]");
173+
() -> "folder is not an instance of [" + IMAPFolder.class.getName() + "]");
169174
IMAPFolder imapFolder = (IMAPFolder) folder;
170175
if (imapFolder.hasNewMessages()) {
171176
return;
172177
}
173-
else if (!folder.getPermanentFlags().contains(Flags.Flag.RECENT)) {
174-
if (searchForNewMessages().length > 0) {
175-
return;
176-
}
178+
else if (!folder.getPermanentFlags().contains(Flags.Flag.RECENT) && searchForNewMessages().length > 0) {
179+
return;
177180
}
178181
imapFolder.addMessageCountListener(this.messageCountListener);
179182
try {
@@ -203,11 +206,11 @@ else if (!folder.getPermanentFlags().contains(Flags.Flag.RECENT)) {
203206
*/
204207
@Override
205208
protected Message[] searchForNewMessages() throws MessagingException {
206-
Flags supportedFlags = this.getFolder().getPermanentFlags();
207-
SearchTerm searchTerm = this.compileSearchTerms(supportedFlags);
208-
Folder folder = this.getFolder();
209-
if (folder.isOpen()) {
210-
return nullSafeMessages(searchTerm != null ? folder.search(searchTerm) : folder.getMessages());
209+
Folder folderToUse = getFolder();
210+
Flags supportedFlags = folderToUse.getPermanentFlags();
211+
SearchTerm searchTerm = compileSearchTerms(supportedFlags);
212+
if (folderToUse.isOpen()) {
213+
return nullSafeMessages(searchTerm != null ? folderToUse.search(searchTerm) : folderToUse.getMessages());
211214
}
212215
throw new MessagingException("Folder is closed");
213216
}
@@ -257,9 +260,11 @@ public void run() {
257260
folder.isOpen(); // resets idle state
258261
}
259262
}
260-
catch (Exception ignore) {
263+
catch (Exception ex) {
264+
logger.error("Error during resetting idle state.", ex);
261265
}
262266
}
267+
263268
}
264269

265270
/**
@@ -327,28 +332,35 @@ public SearchTerm generateSearchTerm(Flags supportedFlags, Folder folder) {
327332
}
328333

329334
if (!recentFlagSupported) {
330-
NotTerm notFlagged = null;
331-
if (folder.getPermanentFlags().contains(Flags.Flag.USER)) {
335+
searchTerm = applyTermsWhenNoRecentFlag(folder, searchTerm);
336+
}
337+
return searchTerm;
338+
}
339+
340+
private SearchTerm applyTermsWhenNoRecentFlag(Folder folder, SearchTerm searchTerm) {
341+
NotTerm notFlagged = null;
342+
if (folder.getPermanentFlags().contains(Flag.USER)) {
343+
if (logger.isDebugEnabled()) {
332344
logger.debug("This email server does not support RECENT flag, but it does support " +
333345
"USER flags which will be used to prevent duplicates during email fetch." +
334346
" This receiver instance uses flag: " + getUserFlag());
335-
Flags siFlags = new Flags();
336-
siFlags.add(getUserFlag());
337-
notFlagged = new NotTerm(new FlagTerm(siFlags, true));
338-
}
339-
else {
340-
logger.debug("This email server does not support RECENT or USER flags. " +
341-
"System flag 'Flag.FLAGGED' will be used to prevent duplicates during email fetch.");
342-
notFlagged = new NotTerm(new FlagTerm(new Flags(Flags.Flag.FLAGGED), true));
343-
}
344-
if (searchTerm == null) {
345-
searchTerm = notFlagged;
346-
}
347-
else {
348-
searchTerm = new AndTerm(searchTerm, notFlagged);
349347
}
348+
Flags siFlags = new Flags();
349+
siFlags.add(getUserFlag());
350+
notFlagged = new NotTerm(new FlagTerm(siFlags, true));
351+
}
352+
else {
353+
logger.debug("This email server does not support RECENT or USER flags. " +
354+
"System flag 'Flag.FLAGGED' will be used to prevent duplicates during email fetch.");
355+
notFlagged = new NotTerm(new FlagTerm(new Flags(Flag.FLAGGED), true));
356+
}
357+
358+
if (searchTerm == null) {
359+
return notFlagged;
360+
}
361+
else {
362+
return new AndTerm(searchTerm, notFlagged);
350363
}
351-
return searchTerm;
352364
}
353365

354366
}

0 commit comments

Comments
 (0)