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