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 */ 018package org.apache.oozie.action.email; 019 020import java.util.ArrayList; 021import java.util.List; 022import java.util.Properties; 023 024import javax.mail.Authenticator; 025import javax.mail.Message; 026import javax.mail.Message.RecipientType; 027import javax.mail.MessagingException; 028import javax.mail.NoSuchProviderException; 029import javax.mail.PasswordAuthentication; 030import javax.mail.Session; 031import javax.mail.Transport; 032import javax.mail.internet.AddressException; 033import javax.mail.internet.InternetAddress; 034import javax.mail.internet.MimeMessage; 035 036import org.apache.oozie.action.ActionExecutor; 037import org.apache.oozie.action.ActionExecutorException; 038import org.apache.oozie.action.ActionExecutorException.ErrorType; 039import org.apache.oozie.client.WorkflowAction; 040import org.apache.oozie.util.XmlUtils; 041import org.jdom.Element; 042import org.jdom.Namespace; 043 044/** 045 * Email action executor. It takes to, cc addresses along with a subject and body and sends 046 * out an email. 047 */ 048public class EmailActionExecutor extends ActionExecutor { 049 050 public static final String CONF_PREFIX = "oozie.email."; 051 public static final String EMAIL_SMTP_HOST = CONF_PREFIX + "smtp.host"; 052 public static final String EMAIL_SMTP_PORT = CONF_PREFIX + "smtp.port"; 053 public static final String EMAIL_SMTP_AUTH = CONF_PREFIX + "smtp.auth"; 054 public static final String EMAIL_SMTP_USER = CONF_PREFIX + "smtp.username"; 055 public static final String EMAIL_SMTP_PASS = CONF_PREFIX + "smtp.password"; 056 public static final String EMAIL_SMTP_FROM = CONF_PREFIX + "from.address"; 057 058 private final static String TO = "to"; 059 private final static String CC = "cc"; 060 private final static String SUB = "subject"; 061 private final static String BOD = "body"; 062 private final static String COMMA = ","; 063 private final static String CONTENT_TYPE = "content_type"; 064 065 private final static String DEFAULT_CONTENT_TYPE = "text/plain"; 066 public EmailActionExecutor() { 067 super("email"); 068 } 069 070 @Override 071 public void initActionType() { 072 super.initActionType(); 073 } 074 075 @Override 076 public void start(Context context, WorkflowAction action) throws ActionExecutorException { 077 try { 078 context.setStartData("-", "-", "-"); 079 Element actionXml = XmlUtils.parseXml(action.getConf()); 080 validateAndMail(context, actionXml); 081 context.setExecutionData("OK", null); 082 } 083 catch (Exception ex) { 084 throw convertException(ex); 085 } 086 } 087 088 @SuppressWarnings("unchecked") 089 protected void validateAndMail(Context context, Element element) throws ActionExecutorException { 090 // The XSD does the min/max occurrence validation for us. 091 Namespace ns = element.getNamespace(); 092 String tos[] = new String[0]; 093 String ccs[] = new String[0]; 094 String subject = ""; 095 String body = ""; 096 String contentType; 097 Element child = null; 098 099 // <to> - One ought to exist. 100 String text = element.getChildTextTrim(TO, ns); 101 if (text.isEmpty()) { 102 throw new ActionExecutorException(ErrorType.ERROR, "EM001", "No receipents were specified in the to-address field."); 103 } 104 tos = text.split(COMMA); 105 106 // <cc> - Optional, but only one ought to exist. 107 try { 108 ccs = element.getChildTextTrim(CC, ns).split(COMMA); 109 } catch (Exception e) { 110 // It is alright for cc to be given empty or not be present. 111 ccs = new String[0]; 112 } 113 114 // <subject> - One ought to exist. 115 subject = element.getChildTextTrim(SUB, ns); 116 117 // <body> - One ought to exist. 118 body = element.getChildTextTrim(BOD, ns); 119 120 contentType = element.getChildTextTrim(CONTENT_TYPE, ns); 121 if (contentType == null || contentType.isEmpty()) { 122 contentType = DEFAULT_CONTENT_TYPE; 123 } 124 125 126 // All good - lets try to mail! 127 email(context, tos, ccs, subject, body, contentType); 128 } 129 130 protected void email(Context context, String[] to, String[] cc, String subject, String body, String contentType) 131 throws ActionExecutorException { 132 // Get mailing server details. 133 String smtpHost = getOozieConf().get(EMAIL_SMTP_HOST, "localhost"); 134 String smtpPort = getOozieConf().get(EMAIL_SMTP_PORT, "25"); 135 Boolean smtpAuth = getOozieConf().getBoolean(EMAIL_SMTP_AUTH, false); 136 String smtpUser = getOozieConf().get(EMAIL_SMTP_USER, ""); 137 String smtpPassword = getOozieConf().get(EMAIL_SMTP_PASS, ""); 138 String fromAddr = getOozieConf().get(EMAIL_SMTP_FROM, "oozie@localhost"); 139 140 Properties properties = new Properties(); 141 properties.setProperty("mail.smtp.host", smtpHost); 142 properties.setProperty("mail.smtp.port", smtpPort); 143 properties.setProperty("mail.smtp.auth", smtpAuth.toString()); 144 145 Session session; 146 // Do not use default instance (i.e. Session.getDefaultInstance) 147 // (cause it may lead to issues when used second time). 148 if (!smtpAuth) { 149 session = Session.getInstance(properties); 150 } else { 151 session = Session.getInstance(properties, new JavaMailAuthenticator(smtpUser, smtpPassword)); 152 } 153 154 Message message = new MimeMessage(session); 155 InternetAddress from; 156 List<InternetAddress> toAddrs = new ArrayList<InternetAddress>(to.length); 157 List<InternetAddress> ccAddrs = new ArrayList<InternetAddress>(cc.length); 158 159 try { 160 from = new InternetAddress(fromAddr); 161 message.setFrom(from); 162 } catch (AddressException e) { 163 throw new ActionExecutorException(ErrorType.ERROR, "EM002", "Bad from address specified in ${oozie.email.from.address}.", e); 164 } catch (MessagingException e) { 165 throw new ActionExecutorException(ErrorType.ERROR, "EM003", "Error setting a from address in the message.", e); 166 } 167 168 try { 169 // Add all <to> 170 for (String toStr : to) { 171 toAddrs.add(new InternetAddress(toStr.trim())); 172 } 173 message.addRecipients(RecipientType.TO, toAddrs.toArray(new InternetAddress[0])); 174 175 // Add all <cc> 176 for (String ccStr : cc) { 177 ccAddrs.add(new InternetAddress(ccStr.trim())); 178 } 179 message.addRecipients(RecipientType.CC, ccAddrs.toArray(new InternetAddress[0])); 180 181 // Set subject 182 message.setSubject(subject); 183 message.setContent(body, contentType); 184 } catch (AddressException e) { 185 throw new ActionExecutorException(ErrorType.ERROR, "EM004", "Bad address format in <to> or <cc>.", e); 186 } catch (MessagingException e) { 187 throw new ActionExecutorException(ErrorType.ERROR, "EM005", "An error occured while adding recipients.", e); 188 } 189 190 try { 191 // Send over SMTP Transport 192 // (Session+Message has adequate details.) 193 Transport.send(message); 194 } catch (NoSuchProviderException e) { 195 throw new ActionExecutorException(ErrorType.ERROR, "EM006", "Could not find an SMTP transport provider to email.", e); 196 } catch (MessagingException e) { 197 throw new ActionExecutorException(ErrorType.ERROR, "EM007", "Encountered an error while sending the email message over SMTP.", e); 198 } 199 } 200 201 @Override 202 public void end(Context context, WorkflowAction action) throws ActionExecutorException { 203 String externalStatus = action.getExternalStatus(); 204 WorkflowAction.Status status = externalStatus.equals("OK") ? WorkflowAction.Status.OK : 205 WorkflowAction.Status.ERROR; 206 context.setEndData(status, getActionSignal(status)); 207 } 208 209 @Override 210 public void check(Context context, WorkflowAction action) 211 throws ActionExecutorException { 212 213 } 214 215 @Override 216 public void kill(Context context, WorkflowAction action) 217 throws ActionExecutorException { 218 219 } 220 221 @Override 222 public boolean isCompleted(String externalStatus) { 223 return true; 224 } 225 226 public static class JavaMailAuthenticator extends Authenticator { 227 228 String user; 229 String password; 230 231 public JavaMailAuthenticator(String user, String password) { 232 this.user = user; 233 this.password = password; 234 } 235 236 @Override 237 protected PasswordAuthentication getPasswordAuthentication() { 238 return new PasswordAuthentication(user, password); 239 } 240 } 241}