001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.email;
019
020import io.stallion.Context;
021import io.stallion.exceptions.ConfigException;
022import io.stallion.exceptions.ValidationException;
023import io.stallion.services.TransactionLog;
024import io.stallion.services.TransactionLogController;
025import io.stallion.settings.Settings;
026import io.stallion.settings.childSections.EmailSettings;
027import io.stallion.services.Log;
028import io.stallion.testing.Stubbing;
029import io.stallion.utils.DateUtils;
030import io.stallion.utils.GeneralUtils;
031
032import javax.mail.Message;
033import javax.mail.MessagingException;
034import javax.mail.Session;
035import javax.mail.Transport;
036import javax.mail.internet.InternetAddress;
037import javax.mail.internet.MimeMessage;
038import java.util.ArrayList;
039import java.util.List;
040import java.util.Map;
041import java.util.Properties;
042import java.util.regex.Pattern;
043
044import static io.stallion.utils.Literals.empty;
045import static io.stallion.utils.Literals.*;
046
047/**
048 * Helper class for sending an email. Can be used as follows:
049 *
050 * EmailSender
051 *     .newSender()
052 *     .setTo("recipient@domain.com")
053 *     .setFrom("sender@domain.com")
054 *     .setSubject("President's Day Sale!")
055 *     .setHtml("...email body email...")
056 *     .send();
057 *
058 * You can create subclasses to implement senders for particular services, such as
059 * Amazon Simple Email Service, Sendgrid, etc.
060 *
061 * The default subclass is SmtpEmailSender() which is used by default throughout Stallion.
062 *
063 */
064public abstract class EmailSender {
065    private List<String> tos;
066    private String from;
067    private String subject;
068    private String html;
069    private String text;
070    private String replyTo;
071    private EmailSettings emailSettings;
072    private String customKey = "";
073    private boolean shouldLog = true;
074    private String type = "";
075
076    /**
077     * Create a new instance of the default implementation for this application.
078     * TODO: allow the default implementation class to be configured via settings
079     *
080     * @return
081     */
082    public static EmailSender newSender() {
083        return new SmtpEmailSender();
084    }
085
086    public EmailSender() {
087        if (Context.settings().getEmail() == null) {
088            throw new ConfigException("Email settings section of conf/stallion.toml is empty. You cannot send email.");
089        }
090        init(Context.settings().getEmail());
091    }
092
093    public EmailSender(EmailSettings emailSettings) {
094        init(emailSettings);
095    }
096
097    protected void init(EmailSettings emailSettings) {
098        this.emailSettings = emailSettings;
099    }
100
101    /**
102     * Send the email.
103     *
104     * @return
105     */
106    public boolean send() {
107        String tosString = String.join(",", tos);
108        try {
109            doSend();
110            if (shouldLog) {
111                logEmail();
112            }
113        } catch (EmailSendException ex) {
114            Log.exception(ex, "Error sending email to: " + tosString);
115            return false;
116        } catch (MessagingException ex) {
117            throw new RuntimeException(ex);
118        } catch (ValidationException invalid) {
119            throw new ConfigException(invalid);
120        }
121        return true;
122    }
123
124    protected void logEmail() {
125        Map<String, Object> extra = map();
126        TransactionLog log = new TransactionLog()
127                .setBody(or(getHtml(), getText()))
128                .setSubject(getSubject())
129                .setCustomKey(customKey)
130                .setUserId(Context.getUser().getId())
131                .setOrgId(Context.getUser().getOrgId())
132                .setToAddress(String.join(",", tos))
133                .setCustomKey(customKey)
134                .setType(type)
135                .setCreatedAt(DateUtils.utcNow())
136                ;
137        extra.put("fromAddress", getFrom());
138        extra.put("replyTo", getReplyTo());
139        TransactionLogController.instance().save(log);
140
141
142
143
144    }
145
146    /**
147     * Validate the email settings to make sure that a user, host, password, etc, have been set correctly.
148     *
149     * @throws ValidationException
150     */
151    public void validate() throws ValidationException {
152        if (Context.settings().getEmail() == null) {
153            throw new ValidationException("Email settings section of conf/stallion.toml is empty. You cannot send email.");
154        }
155        if (empty(emailSettings.getUsername())) {
156            throw new ValidationException("No email user name in conf/stallion.toml email settings");
157        }
158        if (empty(emailSettings.getHost())) {
159            throw new ValidationException("No email host in conf/stallion.toml email settings");
160        }
161        if (empty(emailSettings.getPassword())) {
162            throw new ValidationException("No email password in conf/stallion.toml email settings");
163        }
164    }
165
166
167    protected void doSend() throws MessagingException, EmailSendException, ValidationException {
168        validate();
169
170
171        Properties props = System.getProperties();
172        EmailSettings settings = Context.settings().getEmail();
173
174        String host = settings.getHost();
175        props.put("mail.smtp.starttls.enable", settings.getTls().toString().toLowerCase());
176        props.put("mail.smtp.host", host);
177        props.put("mail.smtp.user", settings.getUsername());
178        props.put("mail.smtp.password", settings.getPassword());
179        props.put("mail.smtp.port", settings.getPort().toString());
180        props.put("mail.smtp.auth", "true");
181
182        Session session = javax.mail.Session.getDefaultInstance(props);
183        MimeMessage message = new MimeMessage(session);
184
185        message.setFrom(new InternetAddress(getFrom()));
186
187        // Format the to addresses and convert into an array of InternetAddress[]
188        List<String> tosList = new ArrayList<>();
189        for( int i = 0; i < tos.size(); i++ ) {
190            tosList.add(restrictToAddress(tos.get(i)));
191        }
192        InternetAddress[] toAddress = new InternetAddress[tos.size()];
193        for( int i = 0; i < tosList.size(); i++ ) {
194            toAddress[i] = new InternetAddress(tosList.get(0));
195            i++;
196        }
197
198        String tosString = String.join(",", tosList);
199
200        for( int i = 0; i < toAddress.length; i++) {
201            message.addRecipient(Message.RecipientType.TO, toAddress[i]);
202        }
203
204        message.setSubject(getSubject());
205        //message.setText(text);
206        message.setContent(html, "text/html");
207        Log.info("Sending email to {0} with subject ''{1}'' from {2}", tosString, getSubject(), message.getFrom()[0].toString());
208        executeSend(message, session, settings);
209
210
211    }
212
213    /**
214     * We want to avoid mistakenly emailing real people when running debug mode.
215     * So instead we reroute all emails to an admin address, with a "+" sign for the wildcard.
216     * @param emailAddress
217     * @return
218     */
219    private String restrictToAddress(String emailAddress) {
220        // Running in prod, not in debug mode
221        if (Settings.instance().getDevMode() != true && "prod".equals(Settings.instance().getEnv())) {
222            return emailAddress;
223        }
224        if (Settings.instance().getEmail().getRestrictOutboundEmails() != true) {
225            return emailAddress;
226        }
227        // Empty host, cannot send an email anyways
228        if (empty(Settings.instance().getEmail().getHost())) {
229            return emailAddress;
230        }
231        if (Settings.instance().getEmail().getAllowedOutboundEmails().contains(emailAddress))  {
232            return emailAddress;
233        }
234        if (empty(Settings.instance().getEmail().getAllowedTestingOutboundEmailCompiledPatterns()) &&
235                (empty(Settings.instance().getEmail().getOutboundEmailTestAddress()) || !Settings.instance().getEmail().getOutboundEmailTestAddress().contains("@"))) {
236            throw new ConfigException("You tried to send an email in debug mode with restrictOutboundEmails set to true, but did not define an admin email address nor a outboundEmailTestAddress in your stallion.toml.");
237        }
238        if (!empty(Settings.instance().getEmail().getAllowedTestingOutboundEmailCompiledPatterns())) {
239            for(Pattern pattern: Settings.instance().getEmail().getAllowedTestingOutboundEmailCompiledPatterns()) {
240                if (pattern.matcher(emailAddress).matches()) {
241                    return emailAddress;
242                }
243            }
244        }
245        String[] parts = Settings.instance().getEmail().getOutboundEmailTestAddress().split("@");
246        emailAddress = GeneralUtils.slugify(emailAddress).replace("-", ".");
247        emailAddress = parts[0] + "+" + emailAddress + "@" + parts[1];
248        return emailAddress;
249    }
250
251
252    private void executeSend(MimeMessage message, Session session, EmailSettings settings) throws EmailSendException {
253        try {
254            Stubbing.checkExecuteStub(this, this, message, session, settings);
255        } catch (Stubbing.StubbedOut stubbedOut) {
256            return;
257        }
258        if (empty(settings.getHost())) {
259            throw new ConfigException("No SMTP host configured for sending outbound emails");
260        }
261        try {
262            Transport transport = session.getTransport("smtp");
263            transport.connect(settings.getHost(), settings.getUsername(), settings.getPassword());
264            transport.sendMessage(message, message.getAllRecipients());
265            transport.close();
266        } catch (MessagingException ex) {
267            throw new EmailSendException("Error sending email " + message.toString(), ex);
268        }
269    }
270
271
272    /**
273     * A list of valid email addresses that we are sending to
274     *
275     * @return
276     */
277    public List<String> getTos() {
278        return tos;
279    }
280
281    public EmailSender setTos(List<String> tos) {
282        this.tos = tos;
283        return this;
284    }
285
286    public EmailSender setTo(String ...tos) {
287        this.tos = new ArrayList<>();
288        for (String to: tos) {
289            this.tos.add(to);
290        }
291        return this;
292    }
293
294    /**
295     * The email from address
296     * @return
297     */
298    public String getFrom() {
299        if (empty(from)) {
300            return Settings.instance().getEmail().getAdminEmails().get(0);
301        }
302        return from;
303    }
304
305    public EmailSender setFrom(String from) {
306        this.from = from;
307        return this;
308    }
309
310    /**
311     * The HTML for the email body
312     * @return
313     */
314    public String getHtml() {
315        return html;
316    }
317
318    public EmailSender setHtml(String html) {
319        this.html = html;
320        return this;
321    }
322
323    /**
324     * Plain text version of the email body
325     *
326     * @return
327     */
328    public String getText() {
329        return text;
330    }
331
332    public EmailSender setText(String text) {
333        this.text = text;
334        return this;
335    }
336
337    /**
338     * The reply to address (optional)
339     * @return
340     */
341    public String getReplyTo() {
342        return replyTo;
343    }
344
345    public EmailSender setReplyTo(String replyTo) {
346        this.replyTo = replyTo;
347        return this;
348    }
349
350    /**
351     * The email subject
352     * @return
353     */
354    public String getSubject() {
355        return subject;
356    }
357
358    public EmailSender setSubject(String subject) {
359        this.subject = subject;
360        return this;
361    }
362
363
364    public String getCustomKey() {
365        return customKey;
366    }
367
368    public EmailSender setCustomKey(String customKey) {
369        this.customKey = customKey;
370        return this;
371    }
372
373    public boolean isShouldLog() {
374        return shouldLog;
375    }
376
377    public EmailSender setShouldLog(boolean shouldLog) {
378        this.shouldLog = shouldLog;
379        return this;
380    }
381
382    public String getType() {
383        return type;
384    }
385
386    public EmailSender setType(String type) {
387        this.type = type;
388        return this;
389    }
390}