diff --git a/build.xml b/build.xml index 35ba766f64..6437c63f6e 100644 --- a/build.xml +++ b/build.xml @@ -277,6 +277,12 @@ + + + + + + @@ -507,6 +513,9 @@ + @@ -704,6 +713,7 @@ + diff --git a/fetch.xml b/fetch.xml index 24e8e6df44..288700cdd3 100644 --- a/fetch.xml +++ b/fetch.xml @@ -335,6 +335,12 @@ Set -Ddest=LOCATION on the command line + + + + diff --git a/lib/libraries.properties b/lib/libraries.properties index 9cfc16820e..4904b17774 100644 --- a/lib/libraries.properties +++ b/lib/libraries.properties @@ -42,21 +42,23 @@ bsh.version=2.0b5 commons-net.version=3.8.0 commons-logging.version=1.1 commons-logging-api.version=${commons-logging.version} -js.version=20.1.0 +js.version=21.1.0 js-scriptengine.version=${js.version} hamcrest-core.version=1.3 hamcrest-library.version=${hamcrest-core.version} jai-core.version=1.1.3 jai-codec.version=1.1.3 -jakarta.mail.version=1.6.4 +# Later 1.6 versions call themselves "jakarta.mail" but do not use the namespace yet +javax.mail.version=1.6.2 +jakarta.mail.version=2.0.1 jakarta-regexp.version=1.4 # Later versions of Tomcat provide a jspc task jasper-compiler.version=4.1.36 jasper-runtime.version=${jasper-compiler.version} jdepend.version=2.9.1 jruby.version=1.6.8 -junit.version=4.13.1 -rhino.version=1.7.11 +junit.version=4.13.2 +rhino.version=1.7.13 junit-platform-launcher.version=1.2.0 # Only used for internal tests in Ant project junit-vintage-engine.version=5.2.0 diff --git a/lib/optional/junit-4.13.jar b/lib/optional/junit-4.13.2.jar similarity index 78% rename from lib/optional/junit-4.13.jar rename to lib/optional/junit-4.13.2.jar index acc3c4320b..6da55d8b85 100644 Binary files a/lib/optional/junit-4.13.jar and b/lib/optional/junit-4.13.2.jar differ diff --git a/src/main/org/apache/tools/ant/taskdefs/email/EmailTask.java b/src/main/org/apache/tools/ant/taskdefs/email/EmailTask.java index 54f811743c..21c0772150 100644 --- a/src/main/org/apache/tools/ant/taskdefs/email/EmailTask.java +++ b/src/main/org/apache/tools/ant/taskdefs/email/EmailTask.java @@ -239,7 +239,7 @@ public void addMessage(Message message) throws BuildException { } /** - * Add a from address element. + * Add a "from" address element. * * @param address The address to send from. */ @@ -251,7 +251,7 @@ public void addFrom(EmailAddress address) { } /** - * Shorthand to set the from address element. + * Shorthand to set the "from" address element. * * @param address The address to send mail from. */ @@ -263,7 +263,7 @@ public void setFrom(String address) { } /** - * Add a replyto address element. + * Add a "replyto" address element. * * @param address The address to reply to. * @since Ant 1.6 @@ -273,7 +273,7 @@ public void addReplyTo(EmailAddress address) { } /** - * Shorthand to set the replyto address element. + * Shorthand to set the "replyto" address element. * * @param address The address to which replies should be directed. * @since Ant 1.6 @@ -283,7 +283,7 @@ public void setReplyTo(String address) { } /** - * Add a to address element. + * Add a "to" address element. * * @param address An email address. */ @@ -449,16 +449,9 @@ public void execute() { // prepare for the auto select mechanism boolean autoFound = false; // try MIME format - if (MIME.equals(encoding) - || (AUTO.equals(encoding) && !autoFound)) { + if (MIME.equals(encoding) || AUTO.equals(encoding)) { 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", + mailer = ClasspathUtils.newInstance(getMailerImplementation(), EmailTask.class.getClassLoader(), Mailer.class); autoFound = true; @@ -467,16 +460,16 @@ public void execute() { 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"); + if ((UU.equals(encoding) || PLAIN.equals(encoding)) + && !autoFound) { + // SMTP auth only allowed with MIME mail + if (user != null || password != null) { + throw new BuildException("SMTP auth only possible with MIME mail"); + } + // SSL only allowed with MIME mail + if (ssl || starttls) { + throw new BuildException("SSL and STARTTLS 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) @@ -600,6 +593,32 @@ public void execute() { } } + private String getMailerImplementation() { + //check to make sure that activation.jar + //and mail.jar are available - see bug 31969 + try { + Class.forName("jakarta.activation.DataHandler"); + Class.forName("jakarta.mail.internet.MimeMessage"); + + return "org.apache.tools.ant.taskdefs.email.JakartaMimeMailer"; + } catch (ClassNotFoundException cnfe) { + logBuildException("Could not find Jakarta MIME mail: ", + new BuildException(cnfe)); + } + + try { + Class.forName("javax.activation.DataHandler"); + Class.forName("javax.mail.internet.MimeMessage"); + + return "org.apache.tools.ant.taskdefs.email.MimeMailer"; + } catch (ClassNotFoundException cnfe) { + logBuildException("Could not find MIME mail: ", + new BuildException(cnfe)); + } + + return "org.apache.tools.ant.taskdefs.email.Mailer"; + } + private void logBuildException(String reason, BuildException e) { Throwable t = e.getCause() == null ? e : e.getCause(); log(reason + t.getMessage(), Project.MSG_WARN); diff --git a/src/main/org/apache/tools/ant/taskdefs/email/JakartaMimeMailer.java b/src/main/org/apache/tools/ant/taskdefs/email/JakartaMimeMailer.java new file mode 100644 index 0000000000..eee328c8e6 --- /dev/null +++ b/src/main/org/apache/tools/ant/taskdefs/email/JakartaMimeMailer.java @@ -0,0 +1,341 @@ +/* + * 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 + * + * https://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.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.security.Provider; +import java.security.Security; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.Vector; + +import jakarta.activation.DataHandler; +import jakarta.activation.DataSource; +import jakarta.activation.FileDataSource; +import jakarta.mail.Address; +import jakarta.mail.Authenticator; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.SendFailedException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; + + +/** + * Uses the JavaMail classes to send Mime format email. + * + * @since Ant 1.5 + */ +public class JakartaMimeMailer extends Mailer { + private static final String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory"; + + private static final String GENERIC_ERROR = + "Problem while sending mime mail:"; + + /** Default character set */ + private static final String DEFAULT_CHARSET + = System.getProperty("file.encoding"); + + // To work properly with national charsets we have to use + // implementation of interface jakarta.activation.DataSource + /** + * String data source implementation. + * @since Ant 1.6 + */ + class StringDataSource implements DataSource { + private String data = null; + private String type = null; + private String charset = null; + private ByteArrayOutputStream out; + + @Override + public InputStream getInputStream() throws IOException { + if (data == null && out == null) { + throw new IOException("No data"); + } + if (out != null) { + final String encodedOut = out.toString(charset); + data = (data != null) ? data.concat(encodedOut) : encodedOut; + out = null; + } + return new ByteArrayInputStream(data.getBytes(charset)); + } + + @Override + public OutputStream getOutputStream() throws IOException { + out = (out == null) ? new ByteArrayOutputStream() : out; + return out; + } + + public void setContentType(final String type) { + this.type = type.toLowerCase(Locale.ENGLISH); + } + + @Override + public String getContentType() { + if (type != null && type.indexOf("charset") > 0 + && type.startsWith("text/")) { + return type; + } + // Must be like "text/plain; charset=windows-1251" + return (type != null ? type : "text/plain") + + "; charset=" + charset; + } + + @Override + public String getName() { + return "StringDataSource"; + } + + public void setCharset(final String charset) { + this.charset = charset; + } + + public String getCharset() { + return charset; + } + } + + /** + * Send the email. + * + * @throws BuildException if the email can't be sent. + */ + @Override + public void send() { + try { + final Properties props = new Properties(); + + props.put("mail.smtp.host", host); + props.put("mail.smtp.port", String.valueOf(port)); + + // Aside, the JDK is clearly unaware of the Scottish + // 'session', which involves excessive quantities of + // alcohol :-) + Session sesh; + Authenticator auth = null; + if (SSL) { + try { + final Provider p = + Class.forName("com.sun.net.ssl.internal.ssl.Provider") + .asSubclass(Provider.class).getDeclaredConstructor().newInstance(); + Security.addProvider(p); + } catch (final Exception e) { + throw new BuildException( + "could not instantiate ssl security provider, check that you have JSSE in your classpath"); + } + // SMTP provider + props.put("mail.smtp.socketFactory.class", SSL_FACTORY); + props.put("mail.smtp.socketFactory.fallback", "false"); + props.put("mail.smtps.host", host); + if (isPortExplicitlySpecified()) { + props.put("mail.smtps.port", String.valueOf(port)); + props.put("mail.smtp.socketFactory.port", + String.valueOf(port)); + } + } + if (user != null || password != null) { + props.put("mail.smtp.auth", "true"); + auth = new SimpleAuthenticator(user, password); + } + if (isStartTLSEnabled()) { + props.put("mail.smtp.starttls.enable", "true"); + } + sesh = Session.getInstance(props, auth); + + //create the message + final MimeMessage msg = new MimeMessage(sesh); + final MimeMultipart attachments = new MimeMultipart(); + + //set the sender + if (from.getName() == null) { + msg.setFrom(new InternetAddress(from.getAddress())); + } else { + msg.setFrom(new InternetAddress(from.getAddress(), + from.getName())); + } + // set the reply to addresses + msg.setReplyTo(internetAddresses(replyToList)); + msg.setRecipients(Message.RecipientType.TO, + internetAddresses(toList)); + msg.setRecipients(Message.RecipientType.CC, + internetAddresses(ccList)); + msg.setRecipients(Message.RecipientType.BCC, + internetAddresses(bccList)); + + // Choosing character set of the mail message + // First: looking it from MimeType + String charset = parseCharSetFromMimeType(message.getMimeType()); + if (charset != null) { + // Assign/reassign message charset from MimeType + message.setCharset(charset); + } else { + // Next: looking if charset having explicit definition + charset = message.getCharset(); + if (charset == null) { + // Using default + charset = DEFAULT_CHARSET; + message.setCharset(charset); + } + } + // Using jakarta.activation.DataSource paradigm + final StringDataSource sds = new StringDataSource(); + sds.setContentType(message.getMimeType()); + sds.setCharset(charset); + + if (subject != null) { + msg.setSubject(subject, charset); + } + msg.addHeader("Date", getDate()); + + if (headers != null) { + for (Header h : headers) { + msg.addHeader(h.getName(), h.getValue()); + } + } + final PrintStream out = new PrintStream(sds.getOutputStream()); + message.print(out); + out.close(); + + final MimeBodyPart textbody = new MimeBodyPart(); + textbody.setDataHandler(new DataHandler(sds)); + attachments.addBodyPart(textbody); + + for (File file : files) { + MimeBodyPart body = new MimeBodyPart(); + if (!file.exists() || !file.canRead()) { + throw new BuildException( + "File \"%s\" does not exist or is not readable.", + file.getAbsolutePath()); + } + final FileDataSource fileData = new FileDataSource(file); + final DataHandler fileDataHandler = new DataHandler(fileData); + + body.setDataHandler(fileDataHandler); + body.setFileName(file.getName()); + attachments.addBodyPart(body); + } + msg.setContent(attachments); + try { + // Send the message using SMTP, or SMTPS if the host uses SSL + final Transport transport = sesh.getTransport(SSL ? "smtps" : "smtp"); + transport.connect(host, user, password); + transport.sendMessage(msg, msg.getAllRecipients()); + } catch (final SendFailedException sfe) { + if (!shouldIgnoreInvalidRecipients()) { + throw new BuildException(GENERIC_ERROR, sfe); + } + if (sfe.getValidSentAddresses() == null + || sfe.getValidSentAddresses().length == 0) { + throw new BuildException("Couldn't reach any recipient", + sfe); + } + Address[] invalid = sfe.getInvalidAddresses(); + if (invalid == null) { + invalid = new Address[0]; + } + for (Address address : invalid) { + didntReach(address, "invalid", sfe); + } + Address[] validUnsent = sfe.getValidUnsentAddresses(); + if (validUnsent == null) { + validUnsent = new Address[0]; + } + for (Address address : validUnsent) { + didntReach(address, "valid", sfe); + } + } + } catch (MessagingException | IOException e) { + throw new BuildException(GENERIC_ERROR, e); + } + } + + private static InternetAddress[] internetAddresses(final Vector list) + throws AddressException, UnsupportedEncodingException { + + final List addrs = new ArrayList<>(); + + for (final EmailAddress addr : list) { + final String name = addr.getName(); + addrs.add((name == null) + ? new InternetAddress(addr.getAddress()) + : new InternetAddress(addr.getAddress(), name)); + } + return addrs.toArray(new InternetAddress[addrs.size()]); + } + + private String parseCharSetFromMimeType(final String type) { + if (type == null) { + return null; + } + final int pos = type.indexOf("charset"); + if (pos < 0) { + return null; + } + // Assuming mime type in form "text/XXXX; charset=XXXXXX" + final StringTokenizer token = new StringTokenizer(type.substring(pos), "=; "); + token.nextToken(); // Skip 'charset=' + return token.nextToken(); + } + + private void didntReach(final Address addr, final String category, + final MessagingException ex) { + final String msg = "Failed to send mail to " + category + " address " + + addr + " because of " + ex.getMessage(); + if (task != null) { + task.log(msg, Project.MSG_WARN); + } else { + System.err.println(msg); + } + } + + static class SimpleAuthenticator extends Authenticator { + private String user = null; + private String password = null; + + public SimpleAuthenticator(final String user, final String password) { + this.user = user; + this.password = password; + } + + @Override + public PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(user, password); + } + } +} + diff --git a/src/main/org/apache/tools/ant/util/ClasspathUtils.java b/src/main/org/apache/tools/ant/util/ClasspathUtils.java index 9e9b2ddc54..93cd60666e 100644 --- a/src/main/org/apache/tools/ant/util/ClasspathUtils.java +++ b/src/main/org/apache/tools/ant/util/ClasspathUtils.java @@ -26,6 +26,7 @@ import org.apache.tools.ant.types.Reference; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; // CheckStyle:HideUtilityClassConstructorCheck OFF - bc @@ -251,6 +252,9 @@ public static T newInstance(String className, ClassLoader userDefinedLoader, try { @SuppressWarnings("unchecked") Class clazz = (Class) Class.forName(className, true, userDefinedLoader); + if (Modifier.isAbstract(clazz.getModifiers())) { + throw new BuildException("Abstract class " + className); + } T o = clazz.getDeclaredConstructor().newInstance(); if (!expectedType.isInstance(o)) { throw new BuildException( @@ -262,10 +266,10 @@ public static T newInstance(String className, ClassLoader userDefinedLoader, throw new BuildException("Class not found: " + className, e); } catch (InstantiationException e) { throw new BuildException("Could not instantiate " + className - + ". Specified class should have a no " + "argument constructor.", e); + + ". Specified class should have a no argument constructor.", e); } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { throw new BuildException("Could not instantiate " + className - + ". Specified class should have a " + "public constructor.", e); + + ". Specified class should have a public constructor.", e); } catch (LinkageError e) { throw new BuildException("Class " + className + " could not be loaded because of an invalid dependency.", e);