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}