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.logging.Level; 033import java.util.logging.Logger; 034import org.jgrapes.core.Channel; 035import org.jgrapes.core.Components; 036import org.jgrapes.core.Components.Timer; 037import org.jgrapes.core.Manager; 038import org.jgrapes.core.annotation.Handler; 039import org.jgrapes.core.annotation.HandlerDefinition.ChannelReplacements; 040import org.jgrapes.core.events.Start; 041import org.jgrapes.core.events.Stop; 042import org.jgrapes.mail.events.SendMessage; 043import org.jgrapes.util.Password; 044 045/** 046 * A component that sends mail using a system wide (user independant) 047 * configuration to access the server. 048 * 049 * The component uses [Jakarta Mail](https://eclipse-ee4j.github.io/mail/) 050 * to connect to a mail server. 051 */ 052@SuppressWarnings("PMD.DataflowAnomalyAnalysis") 053public class SystemMailSender extends MailComponent { 054 055 @SuppressWarnings("PMD.FieldNamingConventions") 056 private static final Logger logger 057 = Logger.getLogger(SystemMailSender.class.getName()); 058 059 private Session session; 060 private Transport transport; 061 private Duration maxIdleTime = Duration.ofMinutes(1); 062 private Timer idleTimer; 063 064 /** 065 * Creates a new component with its channel set to itself. 066 */ 067 public SystemMailSender() { 068 // Nothing to do. 069 } 070 071 /** 072 * Creates a new component base with its channel set to the given 073 * channel. As a special case {@link Channel#SELF} can be 074 * passed to the constructor to make the component use itself 075 * as channel. The special value is necessary as you 076 * obviously cannot pass an object to be constructed to its 077 * constructor. 078 * 079 * @param componentChannel the channel that the component's 080 * handlers listen on by default and that 081 * {@link Manager#fire(Event, Channel...)} sends the event to 082 */ 083 public SystemMailSender(Channel componentChannel) { 084 super(componentChannel); 085 } 086 087 /** 088 * Creates a new component base like {@link #SimpleMailSender(Channel)} 089 * but with channel mappings for {@link Handler} annotations. 090 * 091 * @param componentChannel the channel that the component's 092 * handlers listen on by default and that 093 * {@link Manager#fire(Event, Channel...)} sends the event to 094 * @param channelReplacements the channel replacements to apply 095 * to the `channels` elements of the {@link Handler} annotations 096 */ 097 public SystemMailSender(Channel componentChannel, 098 ChannelReplacements channelReplacements) { 099 super(componentChannel, channelReplacements); 100 } 101 102 /** 103 * Sets the mail properties. See 104 * [the Jakarta Mail](https://jakarta.ee/specifications/mail/2.0/apidocs/jakarta.mail/jakarta/mail/package-summary.html) 105 * documentation for available settings. 106 * 107 * @param props the props 108 * @return the mail monitor 109 */ 110 public SystemMailSender setMailProperties(Map<String, String> props) { 111 mailProps.putAll(props); 112 return this; 113 } 114 115 /** 116 * Sets the maximum idle time. An open connection to the mail server 117 * is closed after this time. 118 * 119 * @param maxIdleTime the new max idle time 120 */ 121 public SystemMailSender setMaxIdleTime(Duration maxIdleTime) { 122 this.maxIdleTime = maxIdleTime; 123 return this; 124 } 125 126 /** 127 * Returns the max idle time. 128 * 129 * @return the duration 130 */ 131 public Duration maxIdleTime() { 132 return maxIdleTime; 133 } 134 135 @Override 136 protected void configureComponent(Map<String, String> values) { 137 Optional.ofNullable(values.get("maxIdleTime")) 138 .map(Integer::parseInt).map(Duration::ofSeconds) 139 .ifPresent(d -> setMaxIdleTime(d)); 140 } 141 142 /** 143 * Start the component. 144 * 145 * @param event the event 146 * @throws MessagingException 147 */ 148 @Handler 149 public void onStart(Start event) throws MessagingException { 150 session = Session.getInstance(mailProps, new Authenticator() { 151 @Override 152 protected PasswordAuthentication 153 getPasswordAuthentication() { 154 return new PasswordAuthentication( 155 mailProps.getProperty("mail.user"), 156 password().map(Password::password).map(String::new) 157 .orElse(null)); 158 } 159 }); 160 transport = session.getTransport(); 161 transport.connect(); 162 idleTimer 163 = Components.schedule(timer -> closeConnection(), maxIdleTime); 164 } 165 166 @SuppressWarnings("PMD.GuardLogStatement") 167 private void closeConnection() { 168 synchronized (transport) { 169 if (idleTimer != null) { 170 idleTimer.cancel(); 171 idleTimer = null; 172 } 173 if (transport.isConnected()) { 174 try { 175 transport.close(); 176 } catch (MessagingException e) { 177 logger.log(Level.WARNING, 178 "Cannot close connection: " + e.getMessage(), e); 179 } 180 } 181 } 182 } 183 184 /** 185 * Sends the message as specified by the event. 186 * 187 * @param event the event 188 * @throws MessagingException the messaging exception 189 */ 190 @Handler 191 public void onMessage(SendMessage event) throws MessagingException { 192 synchronized (transport) { 193 if (idleTimer != null) { 194 idleTimer.cancel(); 195 idleTimer = null; 196 } 197 } 198 Message msg = new MimeMessage(session); 199 if (event.from() != null) { 200 msg.setFrom(event.from()); 201 } else { 202 msg.setFrom(); 203 } 204 msg.setRecipients(Message.RecipientType.TO, event.to()); 205 msg.setRecipients(Message.RecipientType.CC, event.cc()); 206 msg.setRecipients(Message.RecipientType.BCC, event.bcc()); 207 msg.setSentDate(new Date()); 208 for (var header : event.headers().entrySet()) { 209 msg.setHeader(header.getKey(), header.getValue()); 210 } 211 msg.setSubject(event.subject()); 212 msg.setContent(event.content()); 213 214 synchronized (transport) { 215 if (!transport.isConnected()) { 216 transport.connect(); 217 } 218 idleTimer 219 = Components.schedule(timer -> closeConnection(), maxIdleTime); 220 } 221 msg.saveChanges(); 222 transport.sendMessage(msg, msg.getAllRecipients()); 223 } 224 225 /** 226 * Stop the monitor. 227 * 228 * @param event the event 229 * @throws MessagingException 230 */ 231 @Handler 232 @SuppressWarnings("PMD.GuardLogStatement") 233 public void onStop(Stop event) { 234 closeConnection(); 235 } 236 237}