001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.apache.oozie.action.email; 020 021import java.io.File; 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.OutputStream; 025import java.net.URI; 026import java.net.URISyntaxException; 027import java.util.ArrayList; 028import java.util.List; 029import java.util.Properties; 030 031import javax.activation.DataHandler; 032import javax.activation.DataSource; 033import javax.mail.Authenticator; 034import javax.mail.Message; 035import javax.mail.Message.RecipientType; 036import javax.mail.MessagingException; 037import javax.mail.Multipart; 038import javax.mail.NoSuchProviderException; 039import javax.mail.PasswordAuthentication; 040import javax.mail.Session; 041import javax.mail.Transport; 042import javax.mail.internet.AddressException; 043import javax.mail.internet.InternetAddress; 044import javax.mail.internet.MimeBodyPart; 045import javax.mail.internet.MimeMessage; 046import javax.mail.internet.MimeMultipart; 047 048import org.apache.hadoop.conf.Configuration; 049import org.apache.hadoop.fs.FileSystem; 050import org.apache.hadoop.fs.Path; 051import org.apache.oozie.action.ActionExecutor; 052import org.apache.oozie.action.ActionExecutorException; 053import org.apache.oozie.action.ActionExecutorException.ErrorType; 054import org.apache.oozie.client.WorkflowAction; 055import org.apache.oozie.service.ConfigurationService; 056import org.apache.oozie.service.HadoopAccessorException; 057import org.apache.oozie.service.Services; 058import org.apache.oozie.service.HadoopAccessorService; 059import org.apache.oozie.util.XLog; 060import org.apache.oozie.util.XmlUtils; 061import org.jdom.Element; 062import org.jdom.Namespace; 063 064/** 065 * Email action executor. It takes to, cc, bcc addresses along with a subject and body and sends 066 * out an email. 067 */ 068public class EmailActionExecutor extends ActionExecutor { 069 070 public static final String CONF_PREFIX = "oozie.email."; 071 public static final String EMAIL_SMTP_HOST = CONF_PREFIX + "smtp.host"; 072 public static final String EMAIL_SMTP_PORT = CONF_PREFIX + "smtp.port"; 073 public static final String EMAIL_SMTP_AUTH = CONF_PREFIX + "smtp.auth"; 074 public static final String EMAIL_SMTP_USER = CONF_PREFIX + "smtp.username"; 075 public static final String EMAIL_SMTP_PASS = CONF_PREFIX + "smtp.password"; 076 public static final String EMAIL_SMTP_FROM = CONF_PREFIX + "from.address"; 077 public static final String EMAIL_SMTP_SOCKET_TIMEOUT_MS = CONF_PREFIX + "smtp.socket.timeout.ms"; 078 public static final String EMAIL_ATTACHMENT_ENABLED = CONF_PREFIX + "attachment.enabled"; 079 080 private final static String TO = "to"; 081 private final static String CC = "cc"; 082 private final static String BCC = "bcc"; 083 private final static String SUB = "subject"; 084 private final static String BOD = "body"; 085 private final static String ATTACHMENT = "attachment"; 086 private final static String COMMA = ","; 087 private final static String CONTENT_TYPE = "content_type"; 088 089 private final static String DEFAULT_CONTENT_TYPE = "text/plain"; 090 private final XLog LOG = XLog.getLog(getClass()); 091 public static final String EMAIL_ATTACHMENT_ERROR_MSG = 092 "\n Note: This email is missing configured email attachments " 093 + "as sending attachments in email action is disabled in the Oozie server. " 094 + "It could be for security compliance with data protection or other reasons"; 095 096 public EmailActionExecutor() { 097 super("email"); 098 } 099 100 @Override 101 public void initActionType() { 102 super.initActionType(); 103 } 104 105 @Override 106 public void start(Context context, WorkflowAction action) throws ActionExecutorException { 107 LOG.info("Starting action"); 108 try { 109 context.setStartData("-", "-", "-"); 110 Element actionXml = XmlUtils.parseXml(action.getConf()); 111 validateAndMail(context, actionXml); 112 context.setExecutionData("OK", null); 113 } 114 catch (Exception ex) { 115 throw convertException(ex); 116 } 117 } 118 119 @SuppressWarnings("unchecked") 120 protected void validateAndMail(Context context, Element element) throws ActionExecutorException { 121 // The XSD does the min/max occurrence validation for us. 122 Namespace ns = element.getNamespace(); 123 String tos[] = new String[0]; 124 String ccs[] = new String[0]; 125 String bccs[] ; 126 String subject = ""; 127 String body = ""; 128 String attachments[] = new String[0]; 129 String contentType; 130 Element child = null; 131 132 // <to> - One ought to exist. 133 String text = element.getChildTextTrim(TO, ns); 134 if (text.isEmpty()) { 135 throw new ActionExecutorException(ErrorType.ERROR, "EM001", "No recipients were specified in the to-address field."); 136 } 137 tos = text.split(COMMA); 138 139 // <cc> - Optional, but only one ought to exist. 140 try { 141 ccs = element.getChildTextTrim(CC, ns).split(COMMA); 142 } catch (Exception e) { 143 // It is alright for cc to be given empty or not be present. 144 ccs = new String[0]; 145 } 146 147 // <bcc> - Optional, but only one ought to exist. 148 try { 149 bccs = element.getChildTextTrim(BCC, ns).split(COMMA); 150 } catch (Exception e) { 151 // It is alright for bcc to be given empty or not be present. 152 bccs = new String[0]; 153 } 154 // <subject> - One ought to exist. 155 subject = element.getChildTextTrim(SUB, ns); 156 157 // <body> - One ought to exist. 158 body = element.getChildTextTrim(BOD, ns); 159 160 // <attachment> - Optional 161 String attachment = element.getChildTextTrim(ATTACHMENT, ns); 162 if(attachment != null) { 163 attachments = attachment.split(COMMA); 164 } 165 166 contentType = element.getChildTextTrim(CONTENT_TYPE, ns); 167 if (contentType == null || contentType.isEmpty()) { 168 contentType = DEFAULT_CONTENT_TYPE; 169 } 170 171 // All good - lets try to mail! 172 email(tos, ccs, bccs, subject, body, attachments, contentType, context.getWorkflow().getUser()); 173 } 174 175 public void email(String[] to, String[] cc, String subject, String body, String[] attachments, 176 String contentType, String user) throws ActionExecutorException { 177 email(to, cc, new String[0], subject, body, attachments, contentType, user); 178 } 179 180 public void email(String[] to, String[] cc, String[] bcc, String subject, String body, String[] attachments, 181 String contentType, String user) throws ActionExecutorException { 182 // Get mailing server details. 183 String smtpHost = ConfigurationService.get(EMAIL_SMTP_HOST); 184 Integer smtpPortInt = ConfigurationService.getInt(EMAIL_SMTP_PORT); 185 Boolean smtpAuthBool = ConfigurationService.getBoolean(EMAIL_SMTP_AUTH); 186 String smtpUser = ConfigurationService.get(EMAIL_SMTP_USER); 187 String smtpPassword = ConfigurationService.getPassword(EMAIL_SMTP_PASS, ""); 188 String fromAddr = ConfigurationService.get(EMAIL_SMTP_FROM); 189 Integer timeoutMillisInt = ConfigurationService.getInt(EMAIL_SMTP_SOCKET_TIMEOUT_MS); 190 191 Properties properties = new Properties(); 192 properties.setProperty("mail.smtp.host", smtpHost); 193 properties.setProperty("mail.smtp.port", smtpPortInt.toString()); 194 properties.setProperty("mail.smtp.auth", smtpAuthBool.toString()); 195 196 // Apply sensible timeouts, as defaults are infinite. See https://s.apache.org/javax-mail-timeouts 197 properties.setProperty("mail.smtp.connectiontimeout", timeoutMillisInt.toString()); 198 properties.setProperty("mail.smtp.timeout", timeoutMillisInt.toString()); 199 properties.setProperty("mail.smtp.writetimeout", timeoutMillisInt.toString()); 200 201 Session session; 202 // Do not use default instance (i.e. Session.getDefaultInstance) 203 // (cause it may lead to issues when used second time). 204 if (!smtpAuthBool) { 205 session = Session.getInstance(properties); 206 } else { 207 session = Session.getInstance(properties, new JavaMailAuthenticator(smtpUser, smtpPassword)); 208 } 209 210 Message message = new MimeMessage(session); 211 InternetAddress from; 212 List<InternetAddress> toAddrs = new ArrayList<InternetAddress>(to.length); 213 List<InternetAddress> ccAddrs = new ArrayList<InternetAddress>(cc.length); 214 List<InternetAddress> bccAddrs = new ArrayList<InternetAddress>(bcc.length); 215 216 try { 217 from = new InternetAddress(fromAddr); 218 message.setFrom(from); 219 } catch (AddressException e) { 220 throw new ActionExecutorException(ErrorType.ERROR, "EM002", 221 "Bad from address specified in ${oozie.email.from.address}.", e); 222 } catch (MessagingException e) { 223 throw new ActionExecutorException(ErrorType.ERROR, "EM003", 224 "Error setting a from address in the message.", e); 225 } 226 227 try { 228 // Add all <to> 229 for (String toStr : to) { 230 toAddrs.add(new InternetAddress(toStr.trim())); 231 } 232 message.addRecipients(RecipientType.TO, toAddrs.toArray(new InternetAddress[0])); 233 234 // Add all <cc> 235 for (String ccStr : cc) { 236 ccAddrs.add(new InternetAddress(ccStr.trim())); 237 } 238 message.addRecipients(RecipientType.CC, ccAddrs.toArray(new InternetAddress[0])); 239 240 // Add all <bcc> 241 for (String bccStr : bcc) { 242 bccAddrs.add(new InternetAddress(bccStr.trim())); 243 } 244 message.addRecipients(RecipientType.BCC, bccAddrs.toArray(new InternetAddress[0])); 245 246 // Set subject 247 message.setSubject(subject); 248 249 // when there is attachment 250 if (attachments != null && attachments.length > 0 && ConfigurationService.getBoolean(EMAIL_ATTACHMENT_ENABLED)) { 251 Multipart multipart = new MimeMultipart(); 252 253 // Set body text 254 MimeBodyPart bodyTextPart = new MimeBodyPart(); 255 bodyTextPart.setText(body); 256 multipart.addBodyPart(bodyTextPart); 257 258 for (String attachment : attachments) { 259 URI attachUri = new URI(attachment); 260 if (attachUri.getScheme() != null && attachUri.getScheme().equals("file")) { 261 throw new ActionExecutorException(ErrorType.ERROR, "EM008", 262 "Encountered an error when attaching a file. A local file cannot be attached:" 263 + attachment); 264 } 265 MimeBodyPart messageBodyPart = new MimeBodyPart(); 266 DataSource source = new URIDataSource(attachUri, user); 267 messageBodyPart.setDataHandler(new DataHandler(source)); 268 messageBodyPart.setFileName(new File(attachment).getName()); 269 multipart.addBodyPart(messageBodyPart); 270 } 271 message.setContent(multipart); 272 } 273 else { 274 if (attachments != null && attachments.length > 0 && !ConfigurationService.getBoolean(EMAIL_ATTACHMENT_ENABLED)) { 275 body = body + EMAIL_ATTACHMENT_ERROR_MSG; 276 } 277 message.setContent(body, contentType); 278 } 279 } 280 catch (AddressException e) { 281 throw new ActionExecutorException(ErrorType.ERROR, "EM004", "Bad address format in <to> or <cc> or <bcc>.", e); 282 } 283 catch (MessagingException e) { 284 throw new ActionExecutorException(ErrorType.ERROR, "EM005", "An error occurred while adding recipients.", e); 285 } 286 catch (URISyntaxException e) { 287 throw new ActionExecutorException(ErrorType.ERROR, "EM008", "Encountered an error when attaching a file", e); 288 } 289 catch (HadoopAccessorException e) { 290 throw new ActionExecutorException(ErrorType.ERROR, "EM008", "Encountered an error when attaching a file", e); 291 } 292 293 try { 294 // Send over SMTP Transport 295 // (Session+Message has adequate details.) 296 Transport.send(message); 297 } catch (NoSuchProviderException e) { 298 throw new ActionExecutorException(ErrorType.ERROR, "EM006", 299 "Could not find an SMTP transport provider to email.", e); 300 } catch (MessagingException e) { 301 throw new ActionExecutorException(ErrorType.ERROR, "EM007", 302 "Encountered an error while sending the email message over SMTP.", e); 303 } 304 LOG.info("Email sent to [{0}]", to); 305 } 306 307 @Override 308 public void end(Context context, WorkflowAction action) throws ActionExecutorException { 309 String externalStatus = action.getExternalStatus(); 310 WorkflowAction.Status status = externalStatus.equals("OK") ? WorkflowAction.Status.OK : 311 WorkflowAction.Status.ERROR; 312 context.setEndData(status, getActionSignal(status)); 313 LOG.info("Action ended with external status [{0}]", action.getExternalStatus()); 314 } 315 316 @Override 317 public void check(Context context, WorkflowAction action) 318 throws ActionExecutorException { 319 320 } 321 322 @Override 323 public void kill(Context context, WorkflowAction action) 324 throws ActionExecutorException { 325 326 } 327 328 @Override 329 public boolean isCompleted(String externalStatus) { 330 return true; 331 } 332 333 public static class JavaMailAuthenticator extends Authenticator { 334 335 String user; 336 String password; 337 338 public JavaMailAuthenticator(String user, String password) { 339 this.user = user; 340 this.password = password; 341 } 342 343 @Override 344 protected PasswordAuthentication getPasswordAuthentication() { 345 return new PasswordAuthentication(user, password); 346 } 347 } 348 349 class URIDataSource implements DataSource{ 350 351 HadoopAccessorService has = Services.get().get(HadoopAccessorService.class); 352 FileSystem fs; 353 URI uri; 354 public URIDataSource(URI uri, String user) throws HadoopAccessorException { 355 this.uri = uri; 356 Configuration fsConf = has.createConfiguration(uri.getAuthority()); 357 fs = has.createFileSystem(user, uri, fsConf); 358 } 359 360 @Override 361 public InputStream getInputStream() throws IOException { 362 return fs.open(new Path(uri)); 363 } 364 365 @Override 366 public OutputStream getOutputStream() throws IOException { 367 return fs.create(new Path(uri)); 368 } 369 370 @Override 371 public String getContentType() { 372 return "application/octet-stream"; 373 } 374 375 @Override 376 public String getName() { 377 return uri.getPath(); 378 } 379 } 380}