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