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(d -> setMaxIdleTime(d)); 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 public void onMessage(SendMailMessage event, Channel channel) 161 throws MessagingException { 162 if (channel instanceof SenderChannel chan) { 163 chan.sendMessage(event); 164 } else { 165 systemChannel.sendMessage(event); 166 } 167 } 168 169 /** 170 * The specific implementation of the {@link MailChannel}. 171 */ 172 protected class SenderChannel extends MailConnectionManager< 173 MailSender.SenderChannel, Event<?>>.AbstractMailChannel { 174 175 private final Session session; 176 private final Transport transport; 177 private Timer idleTimer; 178 179 /** 180 * Instantiates a new monitor channel. 181 * 182 * @param event the event that triggered the creation 183 * @param mainChannel the main channel (of this {@link Subchannel}) 184 * @param sessionProps the session properties 185 * @param password the password 186 * @throws MessagingException the messaging exception 187 */ 188 public SenderChannel(Event<?> event, Channel mainChannel, 189 Properties sessionProps, Optional<Password> password) 190 throws MessagingException { 191 super(event, mainChannel); 192 var passwd = password.map(Password::password).map(String::new) 193 .orElse(null); 194 session = Session.getInstance(sessionProps, new Authenticator() { 195 // Workaround for class loading problem in OSGi with j.m. 2.1. 196 // Authenticator's classpath allows accessing provider's 197 // service. See https://github.com/eclipse-ee4j/mail/issues/631 198 @Override 199 protected PasswordAuthentication 200 getPasswordAuthentication() { 201 return new PasswordAuthentication( 202 sessionProps.getProperty("mail.user"), passwd); 203 } 204 }); 205 transport = session.getTransport(); 206 transport.connect(sessionProps.getProperty("mail.user"), passwd); 207 idleTimer 208 = Components.schedule(timer -> closeConnection(), maxIdleTime); 209 } 210 211 /** 212 * Send the message provided by the event. 213 * 214 * @param event the event 215 * @throws MessagingException 216 */ 217 protected void sendMessage(SendMailMessage event) 218 throws MessagingException { 219 synchronized (transport) { 220 if (idleTimer != null) { 221 idleTimer.cancel(); 222 idleTimer = null; 223 } 224 } 225 Message msg = new MimeMessage(session); 226 if (event.from() != null) { 227 msg.setFrom(event.from()); 228 } else { 229 msg.setFrom(); 230 } 231 msg.setRecipients(Message.RecipientType.TO, event.to()); 232 msg.setRecipients(Message.RecipientType.CC, event.cc()); 233 msg.setRecipients(Message.RecipientType.BCC, event.bcc()); 234 msg.setSentDate(new Date()); 235 for (var header : event.headers().entrySet()) { 236 msg.setHeader(header.getKey(), header.getValue()); 237 } 238 msg.setSubject(event.subject()); 239 msg.setContent(event.content()); 240 241 synchronized (transport) { 242 if (!transport.isConnected()) { 243 transport.connect(); 244 } 245 idleTimer 246 = Components.schedule(timer -> closeConnection(), 247 maxIdleTime); 248 } 249 msg.saveChanges(); 250 transport.sendMessage(msg, msg.getAllRecipients()); 251 } 252 253 @Override 254 public void close() { 255 closeConnection(); 256 super.close(); 257 } 258 259 /** 260 * Close the connection (not the channel). May be reopened 261 * later if closed due to idle time over. 262 */ 263 @SuppressWarnings("PMD.GuardLogStatement") 264 protected void closeConnection() { 265 synchronized (transport) { 266 if (idleTimer != null) { 267 idleTimer.cancel(); 268 idleTimer = null; 269 } 270 if (transport.isConnected()) { 271 try { 272 transport.close(); 273 } catch (MessagingException e) { 274 logger.log(Level.WARNING, 275 "Cannot close connection: " + e.getMessage(), e); 276 } 277 } 278 } 279 } 280 281 } 282 283}