/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.ant.taskdefs.email; import java.io.File; import java.util.StringTokenizer; import java.util.Vector; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; import org.apache.tools.ant.types.EnumeratedAttribute; import org.apache.tools.ant.types.FileSet; import org.apache.tools.ant.types.Path; import org.apache.tools.ant.types.Resource; import org.apache.tools.ant.types.resources.FileProvider; import org.apache.tools.ant.types.resources.FileResource; import org.apache.tools.ant.util.ClasspathUtils; /** * A task to send SMTP email. This is a refactoring of the SendMail and * MimeMail tasks such that both are within a single task. * * @since Ant 1.5 * @ant.task name="mail" category="network" */ public class EmailTask extends Task { private static final int SMTP_PORT = 25; /** Constant to show that the best available mailer should be used. */ public static final String AUTO = "auto"; /** Constant to allow the Mime mailer to be requested */ public static final String MIME = "mime"; /** Constant to allow the UU mailer to be requested */ public static final String UU = "uu"; /** Constant to allow the plaintext mailer to be requested */ public static final String PLAIN = "plain"; /** * Enumerates the encoding constants. */ public static class Encoding extends EnumeratedAttribute { /** * finds the valid encoding values * * @return a list of valid entries */ @Override public String[] getValues() { return new String[] { AUTO, MIME, UU, PLAIN }; } } private String encoding = AUTO; /** host running SMTP */ private String host = "localhost"; private Integer port = null; /** subject field */ private String subject = null; /** any text */ private Message message = null; /** failure flag */ private boolean failOnError = true; private boolean includeFileNames = false; private String messageMimeType = null; private String messageFileInputEncoding; /* special headers */ /** sender */ private EmailAddress from = null; /** replyto */ private Vector<EmailAddress> replyToList = new Vector<>(); /** TO recipients */ private Vector<EmailAddress> toList = new Vector<>(); /** CC (Carbon Copy) recipients */ private Vector<EmailAddress> ccList = new Vector<>(); /** BCC (Blind Carbon Copy) recipients */ private Vector<EmailAddress> bccList = new Vector<>(); /** generic headers */ private Vector<Header> headers = new Vector<>(); /** file list */ private Path attachments = null; /** Character set for MimeMailer*/ private String charset = null; /** User for SMTP auth */ private String user = null; /** Password for SMTP auth */ private String password = null; /** indicate if the user wishes SSL-TLS */ private boolean ssl = false; /** indicate if the user wishes support for STARTTLS */ private boolean starttls = false; /** ignore invalid recipients? */ private boolean ignoreInvalidRecipients = false; /** * Set the user for SMTP auth; this requires JavaMail. * @param user the String username. * @since Ant 1.6 */ public void setUser(String user) { this.user = user; } /** * Set the password for SMTP auth; this requires JavaMail. * @param password the String password. * @since Ant 1.6 */ public void setPassword(String password) { this.password = password; } /** * Set whether to send data over SSL. * @param ssl boolean; if true SSL will be used. * @since Ant 1.6 */ public void setSSL(boolean ssl) { this.ssl = ssl; } /** * Set whether to allow authentication to switch to a TLS * connection via STARTTLS. * @param b boolean; if true STARTTLS will be supported. * @since Ant 1.8.0 */ public void setEnableStartTLS(boolean b) { this.starttls = b; } /** * Set the preferred encoding method. * * @param encoding The encoding (one of AUTO, MIME, UU, PLAIN). */ public void setEncoding(Encoding encoding) { this.encoding = encoding.getValue(); } /** * Set the mail server port. * * @param port The port to use. */ public void setMailport(int port) { this.port = new Integer(port); } /** * Set the host. * * @param host The host to connect to. */ public void setMailhost(String host) { this.host = host; } /** * Set the subject line of the email. * * @param subject Subject of this email. */ public void setSubject(String subject) { this.subject = subject; } /** * Shorthand method to set the message. * * @param message Message body of this email. */ public void setMessage(String message) { if (this.message != null) { throw new BuildException( "Only one message can be sent in an email"); } this.message = new Message(message); this.message.setProject(getProject()); } /** * Shorthand method to set the message from a file. * * @param file The file from which to take the message. */ public void setMessageFile(File file) { if (this.message != null) { throw new BuildException( "Only one message can be sent in an email"); } this.message = new Message(file); this.message.setProject(getProject()); } /** * Shorthand method to set type of the text message, text/plain by default * but text/html or text/xml is quite feasible. * * @param type The new MessageMimeType value. */ public void setMessageMimeType(String type) { this.messageMimeType = type; } /** * Add a message element. * * @param message The message object. * @throws BuildException if a message has already been added. */ public void addMessage(Message message) throws BuildException { if (this.message != null) { throw new BuildException( "Only one message can be sent in an email"); } this.message = message; } /** * Add a from address element. * * @param address The address to send from. */ public void addFrom(EmailAddress address) { if (this.from != null) { throw new BuildException("Emails can only be from one address"); } this.from = address; } /** * Shorthand to set the from address element. * * @param address The address to send mail from. */ public void setFrom(String address) { if (this.from != null) { throw new BuildException("Emails can only be from one address"); } this.from = new EmailAddress(address); } /** * Add a replyto address element. * * @param address The address to reply to. * @since Ant 1.6 */ public void addReplyTo(EmailAddress address) { this.replyToList.add(address); } /** * Shorthand to set the replyto address element. * * @param address The address to which replies should be directed. * @since Ant 1.6 */ public void setReplyTo(String address) { this.replyToList.add(new EmailAddress(address)); } /** * Add a to address element. * * @param address An email address. */ public void addTo(EmailAddress address) { toList.add(address); } /** * Shorthand to set the "to" address element. * * @param list Comma-separated list of addresses. */ public void setToList(String list) { StringTokenizer tokens = new StringTokenizer(list, ","); while (tokens.hasMoreTokens()) { toList.add(new EmailAddress(tokens.nextToken())); } } /** * Add a "cc" address element. * * @param address The email address. */ public void addCc(EmailAddress address) { ccList.add(address); } /** * Shorthand to set the "cc" address element. * * @param list Comma separated list of addresses. */ public void setCcList(String list) { StringTokenizer tokens = new StringTokenizer(list, ","); while (tokens.hasMoreTokens()) { ccList.add(new EmailAddress(tokens.nextToken())); } } /** * Add a "bcc" address element. * * @param address The email address. */ public void addBcc(EmailAddress address) { bccList.add(address); } /** * Shorthand to set the "bcc" address element. * * @param list comma separated list of addresses. */ public void setBccList(String list) { StringTokenizer tokens = new StringTokenizer(list, ","); while (tokens.hasMoreTokens()) { bccList.add(new EmailAddress(tokens.nextToken())); } } /** * Set whether BuildExceptions should be passed back to the core. * * @param failOnError The new FailOnError value. */ public void setFailOnError(boolean failOnError) { this.failOnError = failOnError; } /** * Set the list of files to be attached. * * @param filenames Comma-separated list of files. */ public void setFiles(String filenames) { StringTokenizer t = new StringTokenizer(filenames, ", "); while (t.hasMoreTokens()) { createAttachments() .add(new FileResource(getProject().resolveFile(t.nextToken()))); } } /** * Add a set of files (nested fileset attribute). * * @param fs The fileset. */ public void addFileset(FileSet fs) { createAttachments().add(fs); } /** * Creates a Path as container for attachments. Supports any * filesystem resource-collections that way. * @return the path to be configured. * @since Ant 1.7 */ public Path createAttachments() { if (attachments == null) { attachments = new Path(getProject()); } return attachments.createPath(); } /** * Create a nested header element. * @return a Header instance. */ public Header createHeader() { Header h = new Header(); headers.add(h); return h; } /** * Set whether to include filenames. * * @param includeFileNames Whether to include filenames in the text of the * message. */ public void setIncludefilenames(boolean includeFileNames) { this.includeFileNames = includeFileNames; } /** * Get whether file names should be included. * * @return Identifies whether file names should be included. */ public boolean getIncludeFileNames() { return includeFileNames; } /** * Whether invalid recipients should be ignored (but a warning * will be logged) instead of making the task fail. * * <p>Even with this property set to true the task will still fail * if the mail couldn't be sent to any recipient at all.</p> * * @since Ant 1.8.0 */ public void setIgnoreInvalidRecipients(boolean b) { ignoreInvalidRecipients = b; } /** * Send an email. */ @Override public void execute() { Message savedMessage = message; try { Mailer mailer = null; // prepare for the auto select mechanism boolean autoFound = false; // try MIME format if (MIME.equals(encoding) || (AUTO.equals(encoding) && !autoFound)) { try { //check to make sure that activation.jar //and mail.jar are available - see bug 31969 Class.forName("javax.activation.DataHandler"); Class.forName("javax.mail.internet.MimeMessage"); mailer = ClasspathUtils.newInstance( "org.apache.tools.ant.taskdefs.email.MimeMailer", EmailTask.class.getClassLoader(), Mailer.class); autoFound = true; log("Using MIME mail", Project.MSG_VERBOSE); } catch (BuildException e) { logBuildException("Failed to initialise MIME mail: ", e); } } // SMTP auth only allowed with MIME mail if (!autoFound && ((user != null) || (password != null)) && (UU.equals(encoding) || PLAIN.equals(encoding))) { throw new BuildException("SMTP auth only possible with MIME mail"); } // SSL only allowed with MIME mail if (!autoFound && (ssl || starttls) && (UU.equals(encoding) || PLAIN.equals(encoding))) { throw new BuildException( "SSL and STARTTLS only possible with MIME mail"); } // try UU format if (UU.equals(encoding) || (AUTO.equals(encoding) && !autoFound)) { try { mailer = ClasspathUtils.newInstance( "org.apache.tools.ant.taskdefs.email.UUMailer", EmailTask.class.getClassLoader(), Mailer.class); autoFound = true; log("Using UU mail", Project.MSG_VERBOSE); } catch (BuildException e) { logBuildException("Failed to initialise UU mail: ", e); } } // try plain format if (PLAIN.equals(encoding) || (AUTO.equals(encoding) && !autoFound)) { mailer = new PlainMailer(); autoFound = true; log("Using plain mail", Project.MSG_VERBOSE); } // a valid mailer must be present by now if (mailer == null) { throw new BuildException("Failed to initialise encoding: %s", encoding); } // a valid message is required if (message == null) { message = new Message(); message.setProject(getProject()); } // an address to send from is required if (from == null || from.getAddress() == null) { throw new BuildException("A from element is required"); } // at least one address to send to/cc/bcc is required if (toList.isEmpty() && ccList.isEmpty() && bccList.isEmpty()) { throw new BuildException( "At least one of to, cc or bcc must be supplied"); } // set the mimetype if not done already (and required) if (messageMimeType != null) { if (message.isMimeTypeSpecified()) { throw new BuildException( "The mime type can only be specified in one location"); } message.setMimeType(messageMimeType); } // set the character set if not done already (and required) if (charset != null) { if (message.getCharset() != null) { throw new BuildException( "The charset can only be specified in one location"); } message.setCharset(charset); } message.setInputEncoding(messageFileInputEncoding); // identify which files should be attached Vector<File> files = new Vector<>(); if (attachments != null) { for (Resource r : attachments) { files.add(r.as(FileProvider.class).getFile()); } } // let the user know what's going to happen log("Sending email: " + subject, Project.MSG_INFO); log("From " + from, Project.MSG_VERBOSE); log("ReplyTo " + replyToList, Project.MSG_VERBOSE); log("To " + toList, Project.MSG_VERBOSE); log("Cc " + ccList, Project.MSG_VERBOSE); log("Bcc " + bccList, Project.MSG_VERBOSE); // pass the params to the mailer mailer.setHost(host); if (port != null) { mailer.setPort(port.intValue()); mailer.setPortExplicitlySpecified(true); } else { mailer.setPort(SMTP_PORT); mailer.setPortExplicitlySpecified(false); } mailer.setUser(user); mailer.setPassword(password); mailer.setSSL(ssl); mailer.setEnableStartTLS(starttls); mailer.setMessage(message); mailer.setFrom(from); mailer.setReplyToList(replyToList); mailer.setToList(toList); mailer.setCcList(ccList); mailer.setBccList(bccList); mailer.setFiles(files); mailer.setSubject(subject); mailer.setTask(this); mailer.setIncludeFileNames(includeFileNames); mailer.setHeaders(headers); mailer.setIgnoreInvalidRecipients(ignoreInvalidRecipients); // send the email mailer.send(); // let the user know what happened int count = files.size(); log("Sent email with " + count + " attachment" + (count == 1 ? "" : "s"), Project.MSG_INFO); } catch (BuildException e) { logBuildException("Failed to send email: ", e); if (failOnError) { throw e; } } catch (Exception e) { log("Failed to send email: " + e.getMessage(), Project.MSG_WARN); if (failOnError) { throw new BuildException(e); } } finally { message = savedMessage; } } private void logBuildException(String reason, BuildException e) { Throwable t = e.getCause() == null ? e : e.getCause(); log(reason + t.getMessage(), Project.MSG_WARN); } /** * Sets the character set of mail message. * Will be ignored if mimeType contains ....; Charset=... substring or * encoding is not <code>mime</code>. * @param charset the character encoding to use. * @since Ant 1.6 */ public void setCharset(String charset) { this.charset = charset; } /** * Returns the character set of mail message. * * @return Charset of mail message. * @since Ant 1.6 */ public String getCharset() { return charset; } /** * Sets the encoding to expect when reading the message from a file. * <p>Will be ignored if the message has been specified inline.</p> * @param encoding the name of the charset used * @since Ant 1.9.4 */ public void setMessageFileInputEncoding(String encoding) { messageFileInputEncoding = encoding; } }