001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 2022 Michael N. Lipp 004 * 005 * This program is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License as published by 007 * the Free Software Foundation; either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, but 011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 013 * for more details. 014 * 015 * You should have received a copy of the GNU General Public License along 016 * with this program; if not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jgrapes.mail; 020 021import com.sun.mail.imap.IMAPFolder; 022import jakarta.mail.Authenticator; 023import jakarta.mail.Flags.Flag; 024import jakarta.mail.Folder; 025import jakarta.mail.Message; 026import jakarta.mail.MessagingException; 027import jakarta.mail.NoSuchProviderException; 028import jakarta.mail.PasswordAuthentication; 029import jakarta.mail.Session; 030import jakarta.mail.Store; 031import jakarta.mail.event.MessageCountAdapter; 032import jakarta.mail.event.MessageCountEvent; 033import java.time.Duration; 034import java.util.Map; 035import java.util.Optional; 036import java.util.logging.Level; 037import java.util.logging.Logger; 038import org.jgrapes.core.Channel; 039import org.jgrapes.core.Components; 040import org.jgrapes.core.Components.Timer; 041import org.jgrapes.core.Event; 042import org.jgrapes.core.Manager; 043import org.jgrapes.core.annotation.Handler; 044import org.jgrapes.core.annotation.HandlerDefinition.ChannelReplacements; 045import org.jgrapes.core.events.Start; 046import org.jgrapes.core.events.Stop; 047import org.jgrapes.mail.events.ReceivedMailMessage; 048 049/** 050 * A component that monitors the INBOX of a mail account. 051 * The component uses [Jakarta Mail](https://eclipse-ee4j.github.io/mail/) 052 * to connect to a mail server. 053 */ 054@SuppressWarnings("PMD.DataflowAnomalyAnalysis") 055public class SimpleMailMonitor extends MailComponent { 056 057 @SuppressWarnings("PMD.FieldNamingConventions") 058 private static final Logger logger 059 = Logger.getLogger(SimpleMailMonitor.class.getName()); 060 061 private String password; 062 private Duration maxIdleTime = Duration.ofMinutes(25); 063 private Duration pollInterval = Duration.ofMinutes(5); 064 private Store store; 065 private Thread monitorThread; 066 private boolean running; 067 068 /** 069 * Creates a new component with its channel set to itself. 070 */ 071 public SimpleMailMonitor() { 072 // Nothing to do. 073 } 074 075 /** 076 * Creates a new component base with its channel set to the given 077 * channel. As a special case {@link Channel#SELF} can be 078 * passed to the constructor to make the component use itself 079 * as channel. The special value is necessary as you 080 * obviously cannot pass an object to be constructed to its 081 * constructor. 082 * 083 * @param componentChannel the channel that the component's 084 * handlers listen on by default and that 085 * {@link Manager#fire(Event, Channel...)} sends the event to 086 */ 087 public SimpleMailMonitor(Channel componentChannel) { 088 super(componentChannel); 089 } 090 091 /** 092 * Creates a new component base like {@link #SimpleMailMonitor(Channel)} 093 * but with channel mappings for {@link Handler} annotations. 094 * 095 * @param componentChannel the channel that the component's 096 * handlers listen on by default and that 097 * {@link Manager#fire(Event, Channel...)} sends the event to 098 * @param channelReplacements the channel replacements to apply 099 * to the `channels` elements of the {@link Handler} annotations 100 */ 101 public SimpleMailMonitor(Channel componentChannel, 102 ChannelReplacements channelReplacements) { 103 super(componentChannel, channelReplacements); 104 } 105 106 /** 107 * Sets the password. 108 * 109 * @param password the new password 110 */ 111 public SimpleMailMonitor setPassword(String password) { 112 this.password = password; 113 return this; 114 } 115 116 /** 117 * Sets the mail properties. See 118 * [the Jakarta Mail](https://jakarta.ee/specifications/mail/2.0/apidocs/jakarta.mail/jakarta/mail/package-summary.html) 119 * documentation for available settings. 120 * 121 * @param props the props 122 * @return the mail monitor 123 */ 124 public SimpleMailMonitor setMailProperties(Map<String, String> props) { 125 mailProps.putAll(props); 126 return this; 127 } 128 129 /** 130 * Sets the maximum idle time. A running {@link IMAPFolder#idle()} 131 * is terminated and renewed after this time. 132 * 133 * @param maxIdleTime the new max idle time 134 */ 135 public SimpleMailMonitor setMaxIdleTime(Duration maxIdleTime) { 136 this.maxIdleTime = maxIdleTime; 137 return this; 138 } 139 140 /** 141 * Returns the max idle time. 142 * 143 * @return the duration 144 */ 145 public Duration maxIdleTime() { 146 return maxIdleTime; 147 } 148 149 /** 150 * Sets the poll interval. Polling is used when the idle command 151 * is not available. 152 * 153 * @param pollInterval the pollInterval to set 154 */ 155 public void setPollInterval(Duration pollInterval) { 156 this.pollInterval = pollInterval; 157 } 158 159 /** 160 * Returns the poll interval. 161 * 162 * @return the pollInterval 163 */ 164 public Duration pollInterval() { 165 return pollInterval; 166 } 167 168 @Override 169 protected void configureComponent(Map<String, String> values) { 170 Optional.ofNullable(values.get("password")) 171 .ifPresent(this::setPassword); 172 Optional.ofNullable(values.get("maxIdleTime")) 173 .map(Integer::parseInt).map(Duration::ofSeconds) 174 .ifPresent(d -> setMaxIdleTime(d)); 175 Optional.ofNullable(values.get("pollInterval")) 176 .map(Integer::parseInt).map(Duration::ofSeconds) 177 .ifPresent(d -> setPollInterval(d)); 178 } 179 180 /** 181 * Run the monitor. 182 * 183 * @param event the event 184 * @throws NoSuchProviderException the no such provider exception 185 */ 186 @Handler 187 public void onStart(Start event) throws NoSuchProviderException { 188 Session session 189 = Session.getInstance(mailProps, new Authenticator() { 190 @Override 191 protected PasswordAuthentication 192 getPasswordAuthentication() { 193 return new PasswordAuthentication( 194 mailProps.getProperty("mail.user"), password); 195 } 196 }); 197 store = session.getStore(); 198 199 // Start monitoring 200 running = true; 201 monitorThread = new Thread(() -> monitor()); 202 monitorThread.setDaemon(true); 203 monitorThread.start(); 204 registerAsGenerator(); 205 } 206 207 @SuppressWarnings({ "PMD.GuardLogStatement", "PMD.AvoidDuplicateLiterals" }) 208 private void monitor() { 209 Thread.currentThread().setName(Components.objectName(this)); 210 while (running) { 211 try { 212 store.connect(); 213 Folder folder = store.getFolder("INBOX"); 214 if (folder == null || !folder.exists()) { 215 logger.log(Level.SEVERE, "Store has no INBOX."); 216 throw new IllegalStateException("No INBOX"); 217 } 218 folder.open(Folder.READ_WRITE); 219 processMessages(folder); 220 } catch (MessagingException e) { 221 logger.log(Level.WARNING, 222 "Cannot open INBOX, will retry: " + e.getMessage(), e); 223 closeStore(); 224 try { 225 Thread.sleep(5000); 226 } catch (InterruptedException e1) { 227 Thread.currentThread().interrupt(); 228 break; 229 } 230 } 231 } 232 closeStore(); 233 } 234 235 @SuppressWarnings({ "PMD.GuardLogStatement", "PMD.AvoidDuplicateLiterals" }) 236 private void closeStore() { 237 if (store.isConnected()) { 238 try { 239 store.close(); 240 } catch (MessagingException e) { 241 logger.log(Level.WARNING, 242 "Cannot close store: " + e.getMessage(), e); 243 } 244 } 245 } 246 247 @SuppressWarnings({ "PMD.GuardLogStatement", "PMD.CognitiveComplexity", 248 "PMD.NPathComplexity" }) 249 private void processMessages(Folder folder) { 250 try { 251 // Process existing (and newly arrived while processing) 252 int start = 1; 253 int end = folder.getMessageCount(); 254 while (running && start <= end) { 255 Message[] msgs = folder.getMessages(start, end); 256 for (Message msg : msgs) { 257 processMessage(msg); 258 } 259 // Check if more messages have arrived 260 start = end + 1; 261 end = folder.getMessageCount(); 262 } 263 264 // Add MessageCountListener to listen for new messages. 265 // The listener will only be invoked when we do another 266 // operation such as idle(). 267 folder.addMessageCountListener(new MessageCountAdapter() { 268 @Override 269 public void messagesAdded(MessageCountEvent countEvent) { 270 Message[] msgs = countEvent.getMessages(); 271 for (Message msg : msgs) { 272 processMessage(msg); 273 } 274 } 275 }); 276 boolean canIdle = true; 277 while (running) { 278 if (canIdle) { 279 @SuppressWarnings("PMD.EmptyCatchBlock") 280 Timer idleTimout = Components.schedule(timer -> { 281 try { 282 folder.getMessageCount(); 283 } catch (MessagingException e) { 284 // Just trying to be nice here. 285 } 286 }, maxIdleTime); 287 try { 288 ((IMAPFolder) folder).idle(); 289 } catch (MessagingException e) { 290 canIdle = false; 291 } finally { 292 idleTimout.cancel(); 293 } 294 } 295 if (!canIdle) { 296 try { 297 Thread.sleep(pollInterval.toMillis()); 298 } catch (InterruptedException ex) { 299 Thread.currentThread().interrupt(); 300 continue; 301 } 302 folder.getMessageCount(); 303 } 304 } 305 } catch (MessagingException e) { 306 logger.log(Level.WARNING, 307 "Problem processing messages: " + e.getMessage(), e); 308 } 309 } 310 311 @SuppressWarnings("PMD.GuardLogStatement") 312 private void processMessage(Message msg) { 313 try { 314 if (msg.getFlags().contains(Flag.DELETED)) { 315 return; 316 } 317 fire(new ReceivedMailMessage(msg)); 318 } catch (MessagingException e) { 319 logger.log(Level.WARNING, 320 "Problem processing message: " + e.getMessage(), e); 321 } 322 } 323 324 /** 325 * Stop the monitor. 326 * 327 * @param event the event 328 */ 329 @Handler 330 @SuppressWarnings("PMD.GuardLogStatement") 331 public void onStop(Stop event) { 332 running = false; 333 // interrupt() does not terminate idle(), closing the store does. 334 if (store != null && store.isConnected()) { 335 try { 336 store.close(); 337 } catch (MessagingException e) { 338 logger.log(Level.WARNING, 339 "Cannot close store: " + e.getMessage(), e); 340 } 341 } 342 // In case we don't use idle() but are sleeping. 343 monitorThread.interrupt(); 344 try { 345 monitorThread.join(1000); 346 } catch (InterruptedException e) { 347 // Ignored 348 } 349 unregisterAsGenerator(); 350 } 351 352}