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}