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 jakarta.mail.Authenticator; 022import jakarta.mail.Message; 023import jakarta.mail.MessagingException; 024import jakarta.mail.PasswordAuthentication; 025import jakarta.mail.Session; 026import jakarta.mail.Transport; 027import jakarta.mail.internet.MimeMessage; 028import java.time.Duration; 029import java.util.Date; 030import java.util.Map; 031import java.util.Optional; 032import java.util.Properties; 033import java.util.logging.Level; 034import org.jgrapes.core.Channel; 035import org.jgrapes.core.Components; 036import org.jgrapes.core.Components.Timer; 037import org.jgrapes.core.Event; 038import org.jgrapes.core.Manager; 039import org.jgrapes.core.Subchannel; 040import org.jgrapes.core.annotation.Handler; 041import org.jgrapes.core.events.Start; 042import org.jgrapes.mail.events.OpenMailSender; 043import org.jgrapes.mail.events.SendMailMessage; 044import org.jgrapes.util.Password; 045 046/** 047 * A component that sends mail using a system wide or user specific 048 * connection. 049 * 050 * The system wide connection is created upon the start of the component. 051 * Additional connections can be created by firing events of type 052 * {@link OpenMailSender}. 053 */ 054@SuppressWarnings("PMD.DataflowAnomalyAnalysis") 055public class MailSender 056 extends MailConnectionManager<MailSender.SenderChannel, Event<?>> { 057 058 private Duration maxIdleTime = Duration.ofMinutes(1); 059 private SenderChannel systemChannel; 060 061 /** 062 * Creates a new component base with its channel set to the given 063 * channel. As a special case {@link Channel#SELF} can be 064 * passed to the constructor to make the component use itself 065 * as channel. The special value is necessary as you 066 * obviously cannot pass an object to be constructed to its 067 * constructor. 068 * 069 * @param componentChannel the channel that the component's 070 * handlers listen on by default and that 071 * {@link Manager#fire(Event, Channel...)} sends the event to 072 */ 073 public MailSender(Channel componentChannel) { 074 super(componentChannel); 075 } 076 077 @Override 078 protected boolean connectionsGenerate() { 079 return false; 080 } 081 082 /** 083 * Sets the mail properties. See the Jakarta Mail documentation 084 * for available settings. 085 * 086 * @param props the props 087 * @return the mail monitor 088 */ 089 public MailSender setMailProperties(Map<String, String> props) { 090 mailProps.putAll(props); 091 return this; 092 } 093 094 /** 095 * Sets the maximum idle time. An open connection to the mail server 096 * is closed after this time. 097 * 098 * @param maxIdleTime the new max idle time 099 */ 100 public MailSender setMaxIdleTime(Duration maxIdleTime) { 101 this.maxIdleTime = maxIdleTime; 102 return this; 103 } 104 105 /** 106 * Returns the max idle time. 107 * 108 * @return the duration 109 */ 110 public Duration maxIdleTime() { 111 return maxIdleTime; 112 } 113 114 @Override 115 protected void configureComponent(Map<String, String> values) { 116 Optional.ofNullable(values.get("maxIdleTime")) 117 .map(Integer::parseInt).map(Duration::ofSeconds) 118 .ifPresent(this::setMaxIdleTime); 119 } 120 121 /** 122 * Start the component. 123 * 124 * @param event the event 125 * @throws MessagingException 126 */ 127 @Handler 128 public void onStart(Start event) throws MessagingException { 129 systemChannel = new SenderChannel(event, 130 channel(), mailProps, password()); 131 } 132 133 /** 134 * Open a connection for sending mail as specified by the event. 135 * 136 * Properties configured for the component are used as fallbacks, 137 * so simply sending an event without specific properties opens 138 * another system connection. 139 * 140 * @param event the event 141 * @param channel the channel 142 * @throws MessagingException 143 */ 144 @Handler 145 public void onOpenMailSender(OpenMailSender event, Channel channel) 146 throws MessagingException { 147 Properties sessionProps = new Properties(mailProps); 148 sessionProps.putAll(event.mailProperties()); 149 new SenderChannel(event, channel, sessionProps, 150 event.password().or(this::password)); 151 } 152 153 /** 154 * Sends the message as specified by the event. 155 * 156 * @param event the event 157 * @throws MessagingException the messaging exception 158 */ 159 @Handler 160 @SuppressWarnings("PMD.CompareObjectsWithEquals") 161 public void onMessage(SendMailMessage event, Channel channel) 162 throws MessagingException { 163 if (channel instanceof SenderChannel chan 164 && chan.mailSender() == this) { 165 chan.sendMessage(event); 166 } else { 167 systemChannel.sendMessage(event); 168 } 169 } 170 171 /** 172 * The specific implementation of the {@link MailChannel}. 173 */ 174 protected class SenderChannel extends MailConnectionManager< 175 MailSender.SenderChannel, Event<?>>.AbstractMailChannel { 176 177 private final Session session; 178 private final Transport transport; 179 private Timer idleTimer; 180 181 /** 182 * Instantiates a new monitor channel. 183 * 184 * @param event the event that triggered the creation 185 * @param mainChannel the main channel (of this {@link Subchannel}) 186 * @param sessionProps the session properties 187 * @param password the password 188 * @throws MessagingException the messaging exception 189 */ 190 public SenderChannel(Event<?> event, Channel mainChannel, 191 Properties sessionProps, Optional<Password> password) 192 throws MessagingException { 193 super(event, mainChannel); 194 var passwd = password.map(Password::password).map(String::new) 195 .orElse(null); 196 session = Session.getInstance(sessionProps, new Authenticator() { 197 // Workaround for class loading problem in OSGi with j.m. 2.1. 198 // Authenticator's classpath allows accessing provider's 199 // service. See https://github.com/eclipse-ee4j/mail/issues/631 200 @Override 201 protected PasswordAuthentication 202 getPasswordAuthentication() { 203 return new PasswordAuthentication( 204 sessionProps.getProperty("mail.user"), passwd); 205 } 206 }); 207 transport = session.getTransport(); 208 transport.connect(sessionProps.getProperty("mail.user"), passwd); 209 idleTimer 210 = Components.schedule(timer -> closeConnection(), maxIdleTime); 211 } 212 213 private MailSender mailSender() { 214 return MailSender.this; 215 } 216 217 /** 218 * Send the message provided by the event. 219 * 220 * @param event the event 221 * @throws MessagingException 222 */ 223 protected void sendMessage(SendMailMessage event) 224 throws MessagingException { 225 synchronized (transport) { 226 if (idleTimer != null) { 227 idleTimer.cancel(); 228 idleTimer = null; 229 } 230 } 231 Message msg = new MimeMessage(session); 232 if (event.from() != null) { 233 msg.setFrom(event.from()); 234 } else { 235 msg.setFrom(); 236 } 237 msg.setRecipients(Message.RecipientType.TO, event.to()); 238 msg.setRecipients(Message.RecipientType.CC, event.cc()); 239 msg.setRecipients(Message.RecipientType.BCC, event.bcc()); 240 msg.setSentDate(new Date()); 241 for (var header : event.headers().entrySet()) { 242 msg.setHeader(header.getKey(), header.getValue()); 243 } 244 msg.setSubject(event.subject()); 245 msg.setContent(event.content()); 246 247 synchronized (transport) { 248 if (!transport.isConnected()) { 249 transport.connect(); 250 } 251 idleTimer 252 = Components.schedule(timer -> closeConnection(), 253 maxIdleTime); 254 } 255 msg.saveChanges(); 256 transport.sendMessage(msg, msg.getAllRecipients()); 257 } 258 259 @Override 260 public void close() { 261 closeConnection(); 262 super.close(); 263 } 264 265 /** 266 * Close the connection (not the channel). May be reopened 267 * later if closed due to idle time over. 268 */ 269 @SuppressWarnings("PMD.GuardLogStatement") 270 protected void closeConnection() { 271 synchronized (transport) { 272 if (idleTimer != null) { 273 idleTimer.cancel(); 274 idleTimer = null; 275 } 276 if (transport.isConnected()) { 277 try { 278 transport.close(); 279 } catch (MessagingException e) { 280 logger.log(Level.WARNING, 281 "Cannot close connection: " + e.getMessage(), e); 282 } 283 } 284 } 285 } 286 287 } 288 289}