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}