/* * The MIT License * * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Bruce Chapman, Erik Ramfelt, Jean-Baptiste Quenot, Luca Domenico Milanesio * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.tasks; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import static hudson.Util.fixEmptyAndTrim; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; import hudson.RestrictedSince; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Run; import hudson.model.TaskListener; import hudson.model.User; import hudson.model.UserPropertyDescriptor; import jenkins.plugins.mailer.tasks.i18n.Messages; import hudson.util.FormValidation; import hudson.util.Secret; import hudson.util.XStream2; import jenkins.model.JenkinsLocationConfiguration; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Date; import java.util.Properties; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.regex.Matcher; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.mail.Address; import javax.mail.Authenticator; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.PasswordAuthentication; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import javax.servlet.ServletException; import org.apache.tools.ant.types.selectors.SelectorUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.export.Exported; import jenkins.model.Jenkins; import jenkins.tasks.SimpleBuildStep; import net.sf.json.JSONObject; import org.kohsuke.accmod.restrictions.DoNotUse; /** * {@link Publisher} that sends the build result in e-mail. * * @author Kohsuke Kawaguchi */ public class Mailer extends Notifier implements SimpleBuildStep { protected static final Logger LOGGER = Logger.getLogger(Mailer.class.getName()); /** * Whitespace-separated list of e-mail addresses that represent recipients. */ public String recipients; /** * If true, only the first unstable build will be reported. */ public boolean dontNotifyEveryUnstableBuild; public boolean isNotifyEveryUnstableBuild() { return !dontNotifyEveryUnstableBuild; } /** * If true, individuals will receive e-mails regarding who broke the build. */ public boolean sendToIndividuals; /** * Default Constructor. * * This is left for backward compatibility. */ @Deprecated public Mailer() {} /** * @param recipients * @param notifyEveryUnstableBuild inverted for historical reasons. * @param sendToIndividuals */ @DataBoundConstructor public Mailer(String recipients, boolean notifyEveryUnstableBuild, boolean sendToIndividuals) { this.recipients = recipients; this.dontNotifyEveryUnstableBuild = !notifyEveryUnstableBuild; this.sendToIndividuals = sendToIndividuals; } @Override public void perform(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { if(debug) listener.getLogger().println("Running mailer"); // substitute build parameters EnvVars env = build.getEnvironment(listener); String recip = env.expand(recipients); new MailSender(recip, dontNotifyEveryUnstableBuild, sendToIndividuals, descriptor().getCharset()) { /** Check whether a path (/-separated) will be archived. */ @Override public boolean artifactMatches(String path, AbstractBuild<?,?> build) { // TODO a Notifier runs after a Recorder so it would make more sense to just check actual artifacts, not configuration // (Anyway currently this code would only be called for an AbstractBuild, since otherwise we cannot know what hyperlink to use for a random workspace.) ArtifactArchiver aa = build.getProject().getPublishersList().get(ArtifactArchiver.class); if (aa == null) { LOGGER.finer("No ArtifactArchiver found"); return false; } String artifacts = aa.getArtifacts(); for (String include : artifacts.split("[, ]+")) { String pattern = include.replace(File.separatorChar, '/'); if (pattern.endsWith("/")) { pattern += "**"; } if (SelectorUtils.matchPath(pattern, path)) { LOGGER.log(Level.FINER, "DescriptorImpl.artifactMatches true for {0} against {1}", new Object[] {path, pattern}); return true; } } LOGGER.log(Level.FINER, "DescriptorImpl.artifactMatches for {0} matched none of {1}", new Object[] {path, artifacts}); return false; } }.run(build,listener); } /** * This class does explicit check pointing. */ public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } private static Pattern ADDRESS_PATTERN = Pattern.compile("\\s*([^<]*)<([^>]+)>\\s*"); /** * @deprecated Use {@link #stringToAddress(java.lang.String, java.lang.String)}. */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("1.16") @SuppressFBWarnings(value = "NM_METHOD_NAMING_CONVENTION", justification = "It's deprecated and required for API compatibility") public static InternetAddress StringToAddress(String strAddress, String charset) throws AddressException, UnsupportedEncodingException { return stringToAddress(strAddress, charset); } /** * Converts a string to {@link InternetAddress}. * @param strAddress Address string * @param charset Charset (encoding) to be used * @return {@link InternetAddress} for the specified string * @throws AddressException Malformed address * @throws UnsupportedEncodingException Unsupported encoding * @since TODO */ public static @Nonnull InternetAddress stringToAddress(@Nonnull String strAddress, @Nonnull String charset) throws AddressException, UnsupportedEncodingException { Matcher m = ADDRESS_PATTERN.matcher(strAddress); if(!m.matches()) { return new InternetAddress(strAddress); } String personal = m.group(1); String address = m.group(2); return new InternetAddress(address, personal, charset); } /** * @deprecated as of 1.286 * Use {@link #descriptor()} to obtain the current instance. */ @Restricted(NoExternalUse.class) @RestrictedSince("1.355") @SuppressFBWarnings(value = "MS_PKGPROTECT", justification = "Deprecated API field") public static DescriptorImpl DESCRIPTOR; public static DescriptorImpl descriptor() { // TODO 1.590+ Jenkins.getActiveInstance final Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { throw new IllegalStateException("Jenkins instance is not ready"); } return jenkins.getDescriptorByType(Mailer.DescriptorImpl.class); } @Extension public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> { /** * The default e-mail address suffix appended to the user name found from changelog, * to send e-mails. Null if not configured. */ private String defaultSuffix; /** * Hudson's own URL, to put into the e-mail. * * @deprecated as of 1.4 * Maintained in {@link JenkinsLocationConfiguration} but left here * for compatibility just in case, so as not to lose this information. * This is loaded to {@link JenkinsLocationConfiguration} via the XML file * marshaled with {@link XStream2}. */ private String hudsonUrl; /** * If non-null, use SMTP-AUTH with these information. */ private String smtpAuthUsername; private Secret smtpAuthPassword; /** * The e-mail address that Hudson puts to "From:" field in outgoing e-mails. * Null if not configured. * * @deprecated as of 1.4 * Maintained in {@link JenkinsLocationConfiguration} but left here * for compatibility just in case, so as not to lose this information. */ private String adminAddress; /** * The e-mail address that Jenkins puts to "Reply-To" header in outgoing e-mails. * Null if not configured. */ private String replyToAddress; /** * The SMTP server to use for sending e-mail. Null for default to the environment, * which is usually <tt>localhost</tt>. */ private String smtpHost; /** * If true use SSL on port 465 (standard SMTPS) unless <code>smtpPort</code> is set. */ private boolean useSsl; /** * The SMTP port to use for sending e-mail. Null for default to the environment, * which is usually <tt>25</tt>. */ private String smtpPort; /** * The charset to use for the text and subject. */ private String charset; /** * Used to keep track of number test e-mails. */ private static transient AtomicInteger testEmailCount = new AtomicInteger(0); @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "Writing to a deprecated field") public DescriptorImpl() { load(); DESCRIPTOR = this; } public String getDisplayName() { return Messages.Mailer_DisplayName(); } public String getDefaultSuffix() { return defaultSuffix; } public String getReplyToAddress() { return replyToAddress; } public void setReplyToAddress(String address) { this.replyToAddress = Util.fixEmpty(address); } /** JavaMail session. */ public Session createSession() { return createSession(smtpHost,smtpPort,useSsl,smtpAuthUsername,smtpAuthPassword); } private static Session createSession(String smtpHost, String smtpPort, boolean useSsl, String smtpAuthUserName, Secret smtpAuthPassword) { smtpPort = fixEmptyAndTrim(smtpPort); smtpAuthUserName = fixEmptyAndTrim(smtpAuthUserName); Properties props = new Properties(System.getProperties()); if(fixEmptyAndTrim(smtpHost)!=null) props.put("mail.smtp.host",smtpHost); if (smtpPort!=null) { props.put("mail.smtp.port", smtpPort); } if (useSsl) { /* This allows the user to override settings by setting system properties but * also allows us to use the default SMTPs port of 465 if no port is already set. * It would be cleaner to use smtps, but that's done by calling session.getTransport()... * and thats done in mail sender, and it would be a bit of a hack to get it all to * coordinate, and we can make it work through setting mail.smtp properties. */ if (props.getProperty("mail.smtp.socketFactory.port") == null) { String port = smtpPort==null?"465":smtpPort; props.put("mail.smtp.port", port); props.put("mail.smtp.socketFactory.port", port); } if (props.getProperty("mail.smtp.socketFactory.class") == null) { props.put("mail.smtp.socketFactory.class","javax.net.ssl.SSLSocketFactory"); } props.put("mail.smtp.socketFactory.fallback", "false"); } if(smtpAuthUserName!=null) props.put("mail.smtp.auth","true"); // avoid hang by setting some timeout. props.put("mail.smtp.timeout","60000"); props.put("mail.smtp.connectiontimeout","60000"); return Session.getInstance(props,getAuthenticator(smtpAuthUserName,Secret.toString(smtpAuthPassword))); } private static Authenticator getAuthenticator(final String smtpAuthUserName, final String smtpAuthPassword) { if(smtpAuthUserName==null) return null; return new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(smtpAuthUserName,smtpAuthPassword); } }; } @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { // this code is brain dead smtpHost = nullify(json.getString("smtpServer")); setReplyToAddress(json.getString("replyToAddress")); defaultSuffix = nullify(json.getString("defaultSuffix")); if(json.has("useSMTPAuth")) { JSONObject auth = json.getJSONObject("useSMTPAuth"); smtpAuthUsername = nullify(auth.getString("smtpAuthUserName")); smtpAuthPassword = Secret.fromString(nullify(auth.getString("smtpAuthPasswordSecret"))); } else { smtpAuthUsername = null; smtpAuthPassword = null; } smtpPort = nullify(json.getString("smtpPort")); useSsl = json.getBoolean("useSsl"); charset = json.getString("charset"); if (charset == null || charset.length() == 0) charset = "UTF-8"; save(); return true; } private String nullify(String v) { if(v!=null && v.length()==0) v=null; return v; } public String getSmtpServer() { return smtpHost; } /** * Method added to pass findbugs verification when compiling against 1.642.1 * @return The JenkinsLocationConfiguration object. * @throws IllegalStateException if the object is not available (e.g., Jenkins not fully initialized). */ @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "False positive. See https://sourceforge.net/p/findbugs/bugs/1411/") private JenkinsLocationConfiguration getJenkinsLocationConfiguration() { final JenkinsLocationConfiguration jlc = JenkinsLocationConfiguration.get(); if (jlc == null) { throw new IllegalStateException("JenkinsLocationConfiguration not available"); } return jlc; } /** * @deprecated as of 1.4 * Use {@link JenkinsLocationConfiguration} */ public String getAdminAddress() { return getJenkinsLocationConfiguration().getAdminAddress(); } /** * @deprecated as of 1.4 * Use {@link JenkinsLocationConfiguration} */ public String getUrl() { return getJenkinsLocationConfiguration().getUrl(); } public String getSmtpAuthUserName() { return smtpAuthUsername; } public String getSmtpAuthPassword() { if (smtpAuthPassword==null) return null; return Secret.toString(smtpAuthPassword); } public Secret getSmtpAuthPasswordSecret() { return smtpAuthPassword; } public boolean getUseSsl() { return useSsl; } public String getSmtpPort() { return smtpPort; } public String getCharset() { String c = charset; if (c == null || c.length() == 0) c = "UTF-8"; return c; } public void setDefaultSuffix(String defaultSuffix) { this.defaultSuffix = defaultSuffix; } /** * @deprecated as of 1.4 * Use {@link JenkinsLocationConfiguration} */ public void setHudsonUrl(String hudsonUrl) { getJenkinsLocationConfiguration().setUrl(hudsonUrl); } /** * @deprecated as of 1.4 * Use {@link JenkinsLocationConfiguration} */ public void setAdminAddress(String adminAddress) { getJenkinsLocationConfiguration().setAdminAddress(adminAddress); } public void setSmtpHost(String smtpHost) { this.smtpHost = smtpHost; } public void setUseSsl(boolean useSsl) { this.useSsl = useSsl; } public void setSmtpPort(String smtpPort) { this.smtpPort = smtpPort; } public void setCharset(String chaset) { this.charset = chaset; } public void setSmtpAuth(String userName, String password) { this.smtpAuthUsername = userName; this.smtpAuthPassword = Secret.fromString(password); } @Override public Publisher newInstance(StaplerRequest req, JSONObject formData) throws FormException { Mailer m = (Mailer)super.newInstance(req, formData); if(hudsonUrl==null) { // if Hudson URL is not configured yet, infer some default hudsonUrl = Functions.inferHudsonURL(req); save(); } return m; } public FormValidation doAddressCheck(@QueryParameter String value) { try { new InternetAddress(value); return FormValidation.ok(); } catch (AddressException e) { return FormValidation.error(e.getMessage()); } } public FormValidation doCheckSmtpServer(@QueryParameter String value) { try { if (fixEmptyAndTrim(value)!=null) InetAddress.getByName(value); return FormValidation.ok(); } catch (UnknownHostException e) { return FormValidation.error(Messages.Mailer_Unknown_Host_Name()+value); } } public FormValidation doCheckDefaultSuffix(@QueryParameter String value) { if (value.matches("@[A-Za-z0-9.\\-]+") || fixEmptyAndTrim(value)==null) return FormValidation.ok(); else return FormValidation.error(Messages.Mailer_Suffix_Error()); } /** * Send an email to the admin address * @throws IOException * @throws ServletException * @throws InterruptedException */ public FormValidation doSendTestMail( @QueryParameter String smtpServer, @QueryParameter String adminAddress, @QueryParameter boolean useSMTPAuth, @QueryParameter String smtpAuthUserName, @QueryParameter Secret smtpAuthPasswordSecret, @QueryParameter boolean useSsl, @QueryParameter String smtpPort, @QueryParameter String charset, @QueryParameter String sendTestMailTo) throws IOException, ServletException, InterruptedException { try { // TODO 1.590+ Jenkins.getActiveInstance final Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { throw new IOException("Jenkins instance is not ready"); } if (!useSMTPAuth) { smtpAuthUserName = null; smtpAuthPasswordSecret = null; } MimeMessage msg = new MimeMessage(createSession(smtpServer, smtpPort, useSsl, smtpAuthUserName, smtpAuthPasswordSecret)); msg.setSubject(Messages.Mailer_TestMail_Subject(testEmailCount.incrementAndGet()), charset); msg.setText(Messages.Mailer_TestMail_Content(testEmailCount.get(), jenkins.getDisplayName()), charset); msg.setFrom(stringToAddress(adminAddress, charset)); if (StringUtils.isNotBlank(replyToAddress)) { msg.setReplyTo(new Address[]{stringToAddress(replyToAddress, charset)}); } msg.setSentDate(new Date()); msg.setRecipient(Message.RecipientType.TO, stringToAddress(sendTestMailTo, charset)); Transport.send(msg); return FormValidation.ok(Messages.Mailer_EmailSentSuccessfully()); } catch (MessagingException e) { return FormValidation.errorWithMarkup("<p>"+Messages.Mailer_FailedToSendEmail()+"</p><pre>"+Util.escape(Functions.printThrowable(e))+"</pre>"); } } public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } } /** * Per user property that is e-mail address. */ public static class UserProperty extends hudson.model.UserProperty { /** * The user's e-mail address. * Null to leave it to default. */ private final String emailAddress; public UserProperty(String emailAddress) { this.emailAddress = emailAddress; } @Exported public String getAddress() { if(hasExplicitlyConfiguredAddress()) { return emailAddress; } // try the inference logic return MailAddressResolver.resolve(user); } public String getConfiguredAddress() { if(hasExplicitlyConfiguredAddress()) { return emailAddress; } // try the inference logic return MailAddressResolver.resolveFast(user); } /** * Gets an email address, which have been explicitly configured on the * user's configuration page. * This method also truncates spaces. It is highly recommended to * use {@link #hasExplicitlyConfiguredAddress()} method to check the * option's existence. * @return A trimmed email address. It can be null * @since TODO */ @CheckForNull public String getExplicitlyConfiguredAddress() { return Util.fixEmptyAndTrim(emailAddress); } /** * Has the user configured a value explicitly (true), or is it inferred (false)? */ public boolean hasExplicitlyConfiguredAddress() { return Util.fixEmptyAndTrim(emailAddress)!=null; } @Extension public static final class DescriptorImpl extends UserPropertyDescriptor { public String getDisplayName() { return Messages.Mailer_UserProperty_DisplayName(); } public UserProperty newInstance(User user) { return new UserProperty(null); } @Override public UserProperty newInstance(@CheckForNull StaplerRequest req, JSONObject formData) throws FormException { return new UserProperty(req != null ? req.getParameter("email.address") : null); } } } /** * Debug probe point to be activated by the scripting console. * @deprecated This hack may be removed in future versions */ @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "It may used for debugging purposes. We have to keep it for the sake of the binary copatibility") @Deprecated public static boolean debug = false; }