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}