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}