package hudson.plugins.emailext; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import groovy.lang.Binding; import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyShell; import hudson.EnvVars; import hudson.FilePath; import hudson.Launcher; import hudson.matrix.MatrixAggregatable; import hudson.matrix.MatrixAggregator; import hudson.matrix.MatrixBuild; import hudson.matrix.MatrixRun; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.BuildListener; import hudson.model.Item; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.model.User; import hudson.plugins.emailext.groovy.sandbox.EmailExtScriptTokenMacroWhitelist; import hudson.plugins.emailext.groovy.sandbox.MimeMessageInstanceWhitelist; import hudson.plugins.emailext.groovy.sandbox.PrintStreamInstanceWhitelist; import hudson.plugins.emailext.groovy.sandbox.PropertiesInstanceWhitelist; import hudson.plugins.emailext.groovy.sandbox.TaskListenerInstanceWhitelist; import hudson.plugins.emailext.plugins.ContentBuilder; import hudson.plugins.emailext.plugins.CssInliner; import hudson.plugins.emailext.plugins.EmailTrigger; import hudson.plugins.emailext.plugins.RecipientProvider; import hudson.plugins.emailext.plugins.content.AbstractEvalContent; import hudson.plugins.emailext.plugins.content.EmailExtScript; import hudson.plugins.emailext.plugins.content.TriggerNameContent; import hudson.plugins.emailext.watching.EmailExtWatchAction; import hudson.plugins.emailext.watching.EmailExtWatchJobProperty; import hudson.tasks.BuildStepMonitor; import hudson.tasks.MailMessageIdAction; import hudson.tasks.Notifier; import hudson.tasks.Publisher; import jenkins.model.Jenkins; import org.apache.commons.lang.StringUtils; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException; import org.jenkinsci.plugins.scriptsecurity.sandbox.Whitelist; import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.GroovySandbox; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.ProxyWhitelist; import org.jenkinsci.plugins.scriptsecurity.scripts.ApprovalContext; import org.jenkinsci.plugins.scriptsecurity.scripts.ClasspathEntry; import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval; import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage; import org.jenkinsci.plugins.tokenmacro.TokenMacro; import org.kohsuke.stapler.Ancestor; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.mail.Address; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.SendFailedException; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; import java.net.ConnectException; import java.net.MalformedURLException; import java.net.SocketException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.LinkedHashSet; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * {@link Publisher} that sends notification e-mail. */ public class ExtendedEmailPublisher extends Notifier implements MatrixAggregatable { private static final Logger LOGGER = Logger.getLogger(ExtendedEmailPublisher.class.getName()); private static final String CONTENT_TRANSFER_ENCODING = System.getProperty(ExtendedEmailPublisher.class.getName() + ".Content-Transfer-Encoding"); public static final String DEFAULT_SUBJECT_TEXT = "$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS!"; public static final String DEFAULT_BODY_TEXT = "$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS:\n\n" + "Check console output at $BUILD_URL to view the results."; public static final String DEFAULT_EMERGENCY_REROUTE_TEXT = ""; public static final String PROJECT_DEFAULT_SUBJECT_TEXT = "$PROJECT_DEFAULT_SUBJECT"; public static final String PROJECT_DEFAULT_BODY_TEXT = "$PROJECT_DEFAULT_CONTENT"; /** * A comma-separated list of email recipient that will be used for every * theTrigger. */ public String recipientList = ""; /** * This is the list of email theTriggers that the project has configured */ public List<EmailTrigger> configuredTriggers = new ArrayList<>(); /** * The contentType of the emails for this project (text/html, text/plain, * etc). */ public String contentType; /** * The default subject of the emails for this project. * ($PROJECT_DEFAULT_SUBJECT) */ public String defaultSubject; /** * The default body of the emails for this project. ($PROJECT_DEFAULT_BODY) */ public String defaultContent; /** * The project wide set of attachments. */ public String attachmentsPattern; /** * The project's pre-send script. */ private String presendScript; /** * The project's post-send script. */ private String postsendScript; private List<GroovyScriptPath> classpath; /** * True to attach the log from the build to the email. */ public boolean attachBuildLog; /** * True to compress the log from the build before attaching to the email */ public boolean compressBuildLog; /** * Reply-To value for the e-mail */ public String replyTo; /** * From value for the e-mail */ public String from; /** * If true, save the generated email content to email-ext-message.[txt|html] */ public boolean saveOutput = false; /** * If true, disables the publisher from running. */ public boolean disabled = false; /** * How to theTrigger the email if the project is a matrix project. */ public MatrixTriggerMode matrixTriggerMode; public ExtendedEmailPublisher() { } @Deprecated public ExtendedEmailPublisher(String project_recipient_list, String project_content_type, String project_default_subject, String project_default_content, String project_attachments, String project_presend_script, int project_attach_buildlog, String project_replyto, String project_from, boolean project_save_output, List<EmailTrigger> project_triggers, MatrixTriggerMode matrixTriggerMode) { this(project_recipient_list, project_content_type, project_default_subject, project_default_content, project_attachments, project_presend_script, project_attach_buildlog, project_replyto, project_from, project_save_output, project_triggers, matrixTriggerMode, false, Collections.<GroovyScriptPath>emptyList()); } @DataBoundConstructor public ExtendedEmailPublisher(String project_recipient_list, String project_content_type, String project_default_subject, String project_default_content, String project_attachments, String project_presend_script, int project_attach_buildlog, String project_replyto,String project_from, boolean project_save_output, List<EmailTrigger> project_triggers, MatrixTriggerMode matrixTriggerMode, boolean project_disabled, List<GroovyScriptPath> classpath) { this.recipientList = project_recipient_list; this.contentType = project_content_type; this.defaultSubject = project_default_subject; this.defaultContent = project_default_content; this.attachmentsPattern = project_attachments; setPresendScript(project_presend_script); this.attachBuildLog = project_attach_buildlog > 0; this.compressBuildLog = project_attach_buildlog > 1; this.replyTo = project_replyto; this.from = project_from; this.saveOutput = project_save_output; this.configuredTriggers = project_triggers; this.matrixTriggerMode = matrixTriggerMode; this.disabled = project_disabled; setClasspath(classpath); } public List<GroovyScriptPath> getClasspath() { return classpath; } public void setClasspath(List<GroovyScriptPath> classpath) { if (classpath != null && !classpath.isEmpty() && Jenkins.getActiveInstance().isUseSecurity()) { //Prepare the classpath for approval ScriptApproval scriptApproval = ScriptApproval.get(); ApprovalContext context = ApprovalContext.create().withCurrentUser(); StaplerRequest request = Stapler.getCurrentRequest(); if (request != null) { context = context.withItem(request.findAncestorObject(Item.class)); } for (GroovyScriptPath path : classpath) { URL pUrl = path.asURL(); if (pUrl != null) { //At least we can try to catch some of them, but some might need token expansion try { scriptApproval.configuring(new ClasspathEntry(pUrl.toString()), context); } catch (MalformedURLException e) { //At least we tried, but we shouldn't end up here since path.asURL() would have returned null assert false : e; } } } } this.classpath = classpath; } public void setPresendScript(String presendScript) { presendScript = StringUtils.trim(presendScript); if (!StringUtils.isBlank(presendScript) && !"$DEFAULT_PRESEND_SCRIPT".equals(presendScript)) { ScriptApproval scriptApproval = ScriptApproval.get(); ApprovalContext context = ApprovalContext.create().withCurrentUser(); StaplerRequest request = Stapler.getCurrentRequest(); if (request != null) { Ancestor ancestor = request.findAncestor(Item.class); if (ancestor != null) { context = context.withItem((Item)ancestor.getObject()); } } scriptApproval.configuring(presendScript, GroovyLanguage.get(), context); } this.presendScript = presendScript; } @DataBoundSetter public void setPostsendScript(String postsendScript) { postsendScript = StringUtils.trim(postsendScript); if (!StringUtils.isBlank(postsendScript) && !"$DEFAULT_POSTSEND_SCRIPT".equals(postsendScript)) { ScriptApproval scriptApproval = ScriptApproval.get(); ApprovalContext context = ApprovalContext.create().withCurrentUser(); StaplerRequest request = Stapler.getCurrentRequest(); if (request != null) { Ancestor ancestor = request.findAncestor(Item.class); if (ancestor != null) { context = context.withItem((Item)ancestor.getObject()); } } scriptApproval.configuring(postsendScript, GroovyLanguage.get(), context); } this.postsendScript = postsendScript; } public String getPresendScript() { return presendScript; } public String getPostsendScript() { return postsendScript; } /** * Get the list of configured email theTriggers for this project. * * @return The list of triggers configure for this publisher instance */ public List<EmailTrigger> getConfiguredTriggers() { if (configuredTriggers == null) { configuredTriggers = new ArrayList<>(); } return configuredTriggers; } public MatrixTriggerMode getMatrixTriggerMode() { return matrixTriggerMode == null ? MatrixTriggerMode.BOTH : matrixTriggerMode; } public void setMatrixTriggerMode(MatrixTriggerMode matrixTriggerMode) { this.matrixTriggerMode = matrixTriggerMode; } @Override public Collection<? extends Action> getProjectActions(AbstractProject<?, ?> project) { return Collections.singletonList(new EmailExtWatchAction(project)); } public void debug(PrintStream p, String format, Object... args) { getDescriptor().debug(p, format, args); } @Override public boolean prebuild(AbstractBuild<?, ?> build, BuildListener listener) { debug(listener.getLogger(), "Checking for pre-build"); if (!(build instanceof MatrixRun) || isExecuteOnMatrixNodes()) { debug(listener.getLogger(), "Executing pre-build step"); return _perform(build, null, listener, true); } return true; } @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { debug(listener.getLogger(), "Checking for post-build"); if (!(build instanceof MatrixRun) || isExecuteOnMatrixNodes()) { debug(listener.getLogger(), "Performing post-build step"); return _perform(build, launcher, listener, false); } return true; } private boolean _perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener, boolean forPreBuild) { if (disabled) { listener.getLogger().println("Extended Email Publisher is currently disabled in project settings"); return true; } boolean emailTriggered = false; debug(listener.getLogger(), "Checking if email needs to be generated"); final Multimap<String, EmailTrigger> triggered = ArrayListMultimap.create(); for (EmailTrigger trigger : getConfiguredTriggers()) { if (trigger.isPreBuild() == forPreBuild && trigger.trigger(build, listener)) { String tName = trigger.getDescriptor().getDisplayName(); triggered.put(tName, trigger); listener.getLogger().println("Email was triggered for: " + tName); emailTriggered = true; } } //Go through and remove triggers that are replaced by others List<String> replacedTriggers = new ArrayList<>(); for (Object tName : triggered.keySet()) { String triggerName = (String) tName; for (EmailTrigger trigger : triggered.get(triggerName)) { replacedTriggers.addAll(trigger.getDescriptor().getTriggerReplaceList()); } } for (String triggerName : replacedTriggers) { triggered.removeAll(triggerName); listener.getLogger().println("Trigger " + triggerName + " was overridden by another trigger and will not send an email."); } EmailExtWatchJobProperty jprop = build.getParent().getProperty(EmailExtWatchJobProperty.class); if (jprop != null) { for (String u : jprop.getWatchers()) { User user = User.get(u); if (user != null) { EmailExtWatchAction.UserProperty prop = user.getProperty(EmailExtWatchAction.UserProperty.class); if (prop != null) { final Multimap<String, EmailTrigger> watcherTriggered = ArrayListMultimap.create(); for (EmailTrigger trigger : prop.getTriggers()) { if (trigger.isPreBuild() == forPreBuild && trigger.trigger(build, listener)) { String tName = trigger.getDescriptor().getDisplayName(); watcherTriggered.put(tName, trigger); listener.getLogger().println("Email was triggered for watcher '" + user.getDisplayName() + "' for: " + tName); emailTriggered = true; } } //Go through and remove triggers that are replaced by others replacedTriggers = new ArrayList<>(); for (Object tName : triggered.keySet()) { String triggerName = (String) tName; for (EmailTrigger trigger : triggered.get(triggerName)) { replacedTriggers.addAll(trigger.getDescriptor().getTriggerReplaceList()); } } for (String triggerName : replacedTriggers) { watcherTriggered.removeAll(triggerName); listener.getLogger().println("Trigger " + triggerName + " was overridden by another trigger and will not send an email."); } triggered.putAll(watcherTriggered); } } } } if (emailTriggered && triggered.isEmpty()) { listener.getLogger().println("There is a circular trigger replacement with the email triggers. No email is sent."); return false; } else if (triggered.isEmpty()) { listener.getLogger().println("No emails were triggered."); return true; } for (String triggerName : triggered.keySet()) { for (EmailTrigger trigger : triggered.get(triggerName)) { listener.getLogger().println("Sending email for trigger: " + triggerName); final ExtendedEmailPublisherContext context = new ExtendedEmailPublisherContext(this, build, build.getWorkspace(), launcher, listener); context.setTriggered(triggered); context.setTrigger(trigger); sendMail(context); } } return true; } @SuppressFBWarnings("REC_CATCH_EXCEPTION") boolean sendMail(ExtendedEmailPublisherContext context) { try { MimeMessage msg = createMail(context); debug(context.getListener().getLogger(), "Successfully created MimeMessage"); Address[] allRecipients = msg.getAllRecipients(); int retries = 0; if (allRecipients != null) { if (executePresendScript(context, msg)) { // presend script might have modified recipients: allRecipients = msg.getAllRecipients(); if (StringUtils.isNotBlank(getDescriptor().getEmergencyReroute())) { // clear out all the existing recipients msg.setRecipients(Message.RecipientType.TO, (Address[]) null); msg.setRecipients(Message.RecipientType.CC, (Address[]) null); msg.setRecipients(Message.RecipientType.BCC, (Address[]) null); // and set the emergency reroute msg.setRecipients(Message.RecipientType.TO, getDescriptor().getEmergencyReroute()); } StringBuilder buf = new StringBuilder("Sending email to:"); for (Address a : allRecipients) { buf.append(' ').append(a); } context.getListener().getLogger().println(buf); ExtendedEmailPublisherDescriptor descriptor = getDescriptor(); Session session = descriptor.createSession(); // emergency reroute might have modified recipients: allRecipients = msg.getAllRecipients(); // all email addresses are of type "rfc822", so just take first one: Transport transport = session.getTransport(allRecipients[0]); while (true) { try { transport.connect(); transport.sendMessage(msg, allRecipients); break; } catch (SendFailedException e) { if (e.getNextException() != null && (e.getNextException() instanceof SocketException || e.getNextException() instanceof ConnectException)) { context.getListener().getLogger().println("Socket error sending email, retrying once more in 10 seconds..."); transport.close(); Thread.sleep(10000); } else { Address[] addresses = e.getValidSentAddresses(); if (addresses != null && addresses.length > 0) { buf = new StringBuilder("Successfully sent to the following addresses:"); for (Address a : addresses) { buf.append(' ').append(a); } context.getListener().getLogger().println(buf); } addresses = e.getValidUnsentAddresses(); if (addresses != null && addresses.length > 0) { buf = new StringBuilder("Error sending to the following VALID addresses:"); for (Address a : addresses) { buf.append(' ').append(a); } context.getListener().getLogger().println(buf); } addresses = e.getInvalidAddresses(); if (addresses != null && addresses.length > 0) { buf = new StringBuilder("Error sending to the following INVALID addresses:"); for (Address a : addresses) { buf.append(' ').append(a); } context.getListener().getLogger().println(buf); } debug(context.getListener().getLogger(), "SendFailedException message: " + e.getMessage()); break; } } catch (MessagingException e) { if (e.getNextException() != null && e.getNextException() instanceof ConnectException) { context.getListener().getLogger().println("Connection error sending email, retrying once more in 10 seconds..."); transport.close(); Thread.sleep(10000); } else { debug(context.getListener().getLogger(), "MessagingException message: " + e.getMessage()); break; } } retries++; if (retries > 1) { context.getListener().getLogger().println("Failed after second try sending email"); break; } } executePostsendScript(context, msg, session, transport); // close transport after post-send script, so server response can be accessed: transport.close(); if (context.getRun().getAction(MailMessageIdAction.class) == null) { context.getRun().addAction(new MailMessageIdAction(msg.getMessageID())); } } else { context.getListener().getLogger().println("Email sending was cancelled" + " by user script."); } return true; } else { context.getListener().getLogger().println("An attempt to send an e-mail" + " to empty list of recipients, ignored."); } } catch (Exception e) { LOGGER.log(Level.WARNING, "Could not send email.", e); e.printStackTrace(context.getListener().error("Could not send email as a part of the post-build publishers.")); } debug(context.getListener().getLogger(), "Some error occured trying to send the email...check the Jenkins log"); return false; } public List<TokenMacro> getRuntimeMacros(ExtendedEmailPublisherContext context) { List<TokenMacro> macros = new ArrayList<>(); macros.add(new TriggerNameContent(context.getTrigger().getDescriptor().getDisplayName())); return macros; } private boolean executePresendScript(ExtendedEmailPublisherContext context, MimeMessage msg) throws RuntimeException { return executeScript(presendScript, "pre-send", context, msg, null, null); } private void executePostsendScript(ExtendedEmailPublisherContext context, MimeMessage msg, Session session, Transport transport) throws RuntimeException { executeScript(postsendScript, "post-send", context, msg, session, transport); } private boolean executeScript(String rawScript, String scriptName, ExtendedEmailPublisherContext context, MimeMessage msg, Session session, Transport transport) { boolean cancel = false; String script = ContentBuilder.transformText(rawScript, context, getRuntimeMacros(context)); if (StringUtils.isNotBlank(script)) { TaskListener listener = context.getListener(); PrintStream logger = listener.getLogger(); debug(logger, "Executing %s script", scriptName); CompilerConfiguration cc = GroovySandbox.createSecureCompilerConfiguration(); cc.setScriptBaseClass(EmailExtScript.class.getCanonicalName()); cc.addCompilationCustomizers(new ImportCustomizer().addStarImports( "jenkins", "jenkins.model", "hudson", "hudson.model")); Binding binding = new Binding(); binding.setVariable("build", context.getBuild()); binding.setVariable("run", context.getRun()); binding.setVariable("msg", msg); Properties props = null; if (session != null) { props = session.getProperties(); binding.setVariable("props", props); } if (transport != null) { binding.setVariable("transport", transport); //Can't really figure out what to whitelist in this object } binding.setVariable("listener", listener); binding.setVariable("logger", logger); binding.setVariable("cancel", cancel); binding.setVariable("trigger", context.getTrigger()); binding.setVariable("triggered", ImmutableMultimap.copyOf(context.getTriggered())); //TODO static whitelist? StringWriter out = new StringWriter(); PrintWriter pw = new PrintWriter(out); try { ClassLoader cl = expandClasspath(context, Jenkins.getActiveInstance().getPluginManager().uberClassLoader); GroovyShell shell = new GroovyShell(cl, binding, cc); if (AbstractEvalContent.isApprovedScript(script, GroovyLanguage.get()) || !Jenkins.getActiveInstance().isUseSecurity()) { shell.parse(script).run(); } else { try { GroovySandbox.run(shell.parse(script), new ProxyWhitelist( Whitelist.all(), new MimeMessageInstanceWhitelist(msg), new PropertiesInstanceWhitelist(props), new TaskListenerInstanceWhitelist(listener), new PrintStreamInstanceWhitelist(logger), new EmailExtScriptTokenMacroWhitelist())); } catch (RejectedAccessException x) { throw ScriptApproval.get().accessRejected(x, ApprovalContext.create()); } } cancel = (Boolean)shell.getVariable("cancel"); debug(logger, "%s script set cancel to %b", StringUtils.capitalize(scriptName), cancel); } catch (SecurityException e) { logger.println(StringUtils.capitalize(scriptName) + " script tried to access secured objects: " + e.getMessage()); throw e; } catch (Throwable t) { t.printStackTrace(pw); logger.println(out.toString()); // should we cancel the sending of the email??? } debug(logger, out.toString()); } return !cancel; } /** * Expand the plugin class loader with URL taken from the project descriptor * and the global configuration. * * @param context the current email context * @param loader the class loader to expand * @return the new expanded classloader */ private ClassLoader expandClasspath(ExtendedEmailPublisherContext context, ClassLoader loader) throws IOException { List<ClasspathEntry> classpathList = new ArrayList<>(); if (classpath != null && !classpath.isEmpty()) { transformToClasspathEntries(classpath, context, classpathList); } List<GroovyScriptPath> globalClasspath = getDescriptor().getDefaultClasspath(); if (globalClasspath != null && !globalClasspath.isEmpty()) { transformToClasspathEntries(globalClasspath, context, classpathList); } boolean useSecurity = Jenkins.getActiveInstance().isUseSecurity(); if (!classpathList.isEmpty()) { GroovyClassLoader gloader = new GroovyClassLoader(loader); gloader.setShouldRecompile(true); for (ClasspathEntry entry : classpathList) { if (useSecurity) { ScriptApproval.get().using(entry); } gloader.addURL(entry.getURL()); } loader = gloader; } if (useSecurity) { return GroovySandbox.createSecureClassLoader(loader); } else { return loader; } } private void transformToClasspathEntries(List<GroovyScriptPath> input, ExtendedEmailPublisherContext context, List<ClasspathEntry> output) { for (GroovyScriptPath path : input) { URL url = path.asURL(); if (url != null) { try { ClasspathEntry entry = new ClasspathEntry(url.toString()); output.add(entry); } catch (MalformedURLException e) { context.getListener().getLogger().printf("[email-ext] Warning: Ignoring classpath: [%s] as it could not be transformed into a valid URL%n", path.getPath()); } } } } private MimeMessage createMail(ExtendedEmailPublisherContext context) throws MessagingException, IOException, InterruptedException { ExtendedEmailPublisherDescriptor descriptor = getDescriptor(); if (!descriptor.getOverrideGlobalSettings()) { descriptor.upgradeFromMailer(); } String charset = descriptor.getCharset(); Session session = descriptor.createSession(); MimeMessage msg = new MimeMessage(session); InternetAddress fromAddress = new InternetAddress(descriptor.getAdminAddress()); if (fromAddress.getPersonal() != null) { fromAddress.setPersonal(fromAddress.getPersonal(), charset); } if (StringUtils.isNotBlank(from)) { fromAddress = new InternetAddress(from); } msg.setFrom(fromAddress); if (descriptor.isDebugMode()) { session.setDebug(true); session.setDebugOut(context.getListener().getLogger()); } // Set the contents of the email msg.addHeader("X-Jenkins-Job", context.getRun().getParent().getDisplayName()); final Result result = context.getRun() != null ? context.getRun().getResult() : null; if (result != null) { msg.addHeader("X-Jenkins-Result", result.toString()); } msg.setSentDate(new Date()); setSubject(context, msg, charset); Multipart multipart = addContent(context, charset); AttachmentUtils attachments = new AttachmentUtils(attachmentsPattern); attachments.attach(multipart, context); // add attachments from the email type if they are setup if (StringUtils.isNotBlank(context.getTrigger().getEmail().getAttachmentsPattern())) { AttachmentUtils typeAttachments = new AttachmentUtils(context.getTrigger().getEmail().getAttachmentsPattern()); typeAttachments.attach(multipart, context); } if (attachBuildLog || context.getTrigger().getEmail().getAttachBuildLog()) { debug(context.getListener().getLogger(), "Request made to attach build log"); AttachmentUtils.attachBuildLog(context, multipart, compressBuildLog || context.getTrigger().getEmail().getCompressBuildLog()); } msg.setContent(multipart); EnvVars env = null; try { env = context.getRun().getEnvironment(context.getListener()); } catch (Exception e) { context.getListener().getLogger().println("Error retrieving environment vars: " + e.getMessage()); // create an empty set of env vars env = new EnvVars(); } // Get the recipients from the global list of addresses Set<InternetAddress> to = new LinkedHashSet<>(); Set<InternetAddress> cc = new LinkedHashSet<>(); Set<InternetAddress> bcc = new LinkedHashSet<>(); String emergencyReroute = descriptor.getEmergencyReroute(); if (StringUtils.isNotBlank(emergencyReroute)) { debug(context.getListener().getLogger(), "Emergency reroute turned on"); EmailRecipientUtils.addAddressesFromRecipientList(to, cc, bcc, emergencyReroute, env, context.getListener()); debug(context.getListener().getLogger(), "Emergency reroute is set to: " + emergencyReroute); } else { for (RecipientProvider provider : context.getTrigger().getEmail().getRecipientProviders()) { provider.addRecipients(context, env, to, cc, bcc); } descriptor.debug(context.getListener().getLogger(), "Adding recipients from trigger recipient list"); EmailRecipientUtils.addAddressesFromRecipientList(to, cc, bcc, EmailRecipientUtils.getRecipientList(context, context.getTrigger().getEmail().getRecipientList()), env, context.getListener()); } // remove the excluded recipients Set<InternetAddress> excludedRecipients = new LinkedHashSet<>(); for (InternetAddress recipient : to) { if (EmailRecipientUtils.isExcludedRecipient(recipient.getAddress(), context.getListener())) { excludedRecipients.add(recipient); } } to.removeAll(excludedRecipients); cc.removeAll(excludedRecipients); bcc.removeAll(excludedRecipients); msg.setRecipients(Message.RecipientType.TO, to.toArray(new InternetAddress[to.size()])); if (!cc.isEmpty()) { msg.setRecipients(Message.RecipientType.CC, cc.toArray(new InternetAddress[cc.size()])); } if (!bcc.isEmpty()) { msg.setRecipients(Message.RecipientType.BCC, bcc.toArray(new InternetAddress[bcc.size()])); } Set<InternetAddress> replyToAddresses = new LinkedHashSet<>(); if (StringUtils.isNotBlank(replyTo)) { EmailRecipientUtils.addAddressesFromRecipientList(replyToAddresses, null, null, EmailRecipientUtils.getRecipientList(context, replyTo), env, context.getListener()); } if (StringUtils.isNotBlank(context.getTrigger().getEmail().getReplyTo())) { EmailRecipientUtils.addAddressesFromRecipientList(replyToAddresses, null, null, EmailRecipientUtils.getRecipientList(context, context.getTrigger().getEmail().getReplyTo()), env, context.getListener()); } if (!replyToAddresses.isEmpty()) { msg.setReplyTo(replyToAddresses.toArray(new InternetAddress[replyToAddresses.size()])); } Run<?, ?> pb = getPreviousRun(context.getRun(), context.getListener()); if (pb != null) { // Send mails as replies until next successful build MailMessageIdAction b = pb.getAction(MailMessageIdAction.class); if (b != null && pb.getResult() != Result.SUCCESS) { debug(context.getListener().getLogger(), "Setting In-Reply-To since last build was not successful"); msg.setHeader("In-Reply-To", b.messageId); msg.setHeader("References", b.messageId); } } if (CONTENT_TRANSFER_ENCODING != null) { msg.setHeader("Content-Transfer-Encoding", CONTENT_TRANSFER_ENCODING); } String listId = descriptor.getListId(); if (listId != null) { msg.setHeader("List-ID", listId); } if (descriptor.getPrecedenceBulk()) { msg.setHeader("Precedence", "bulk"); } return msg; } private void setSubject(ExtendedEmailPublisherContext context, MimeMessage msg, String charset) throws MessagingException { String subject = ContentBuilder.transformText(context.getTrigger().getEmail().getSubject(), context, getRuntimeMacros(context)); msg.setSubject(subject, charset); } public boolean isExecuteOnMatrixNodes() { MatrixTriggerMode mtm = getMatrixTriggerMode(); return MatrixTriggerMode.BOTH == mtm || MatrixTriggerMode.ONLY_CONFIGURATIONS == mtm; } private Multipart addContent(ExtendedEmailPublisherContext context, String charset) throws MessagingException { final String text = ContentBuilder.transformText(context.getTrigger().getEmail().getBody(), context, getRuntimeMacros(context)); final Multipart multipart; boolean doBoth = false; String messageContentType = context.getTrigger().getEmail().getContentType().equals("project") ? contentType : context.getTrigger().getEmail().getContentType(); // contentType is null if the project was not reconfigured after upgrading. if (messageContentType == null || "default".equals(messageContentType)) { messageContentType = getDescriptor().getDefaultContentType(); // The defaultContentType is null if the main Jenkins configuration // was not reconfigured after upgrading. if (messageContentType == null) { messageContentType = "text/plain"; } } if ("both".equals(messageContentType)) { doBoth = true; multipart = new MimeMultipart("alternative"); messageContentType = "text/html"; } else { multipart = new MimeMultipart(); } messageContentType += "; charset=" + charset; try { if (saveOutput) { String extension = ".html"; if (messageContentType.startsWith("text/plain")) { extension = ".txt"; } FilePath workspace = context.getWorkspace(); if (workspace != null) { FilePath savedOutput = new FilePath(workspace, String.format("%s-%s%s", context.getTrigger().getDescriptor().getDisplayName(), context.getRun().getId(), extension)); savedOutput.write(text, charset); } else { context.getListener().getLogger().println("No workspace to save the email to"); } } } catch (IOException | InterruptedException e) { context.getListener().getLogger().println("Error trying to save email output to file. " + e.getMessage()); } // set the email message text // (plain text or HTML depending on the content type) MimeBodyPart msgPart = new MimeBodyPart(); debug(context.getListener().getLogger(), "messageContentType = %s", messageContentType); if (messageContentType.startsWith("text/html")) { CssInliner inliner = new CssInliner(); if (doBoth) { MimeBodyPart plainTextPart = new MimeBodyPart(); plainTextPart.setContent(inliner.stripHtml(text), "text/plain; charset=" + charset); multipart.addBodyPart(plainTextPart); } String inlinedCssHtml = inliner.process(text); msgPart.setContent(inlinedCssHtml, messageContentType); } else { msgPart.setContent(text, messageContentType); } multipart.addBodyPart(msgPart); return multipart; } @Override public boolean needsToRunAfterFinalized() { return true; } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } /** * Looks for a previous build, so long as that is in fact completed. * Necessary since {@link #getRequiredMonitorService} does not wait for the * previous build, so in the case of parallel-capable jobs, we need to * behave sensibly when a later build actually finishes before an earlier * one. * * @param run a run for which we may be sending mail * @param listener a listener to which we may print warnings in case the * actual previous build is still in progress * @return the previous build, or null if that build is missing, or is still * in progress */ public static @CheckForNull Run<?, ?> getPreviousRun(@Nonnull Run<?, ?> run, TaskListener listener) { Run<?, ?> previousRun = run.getPreviousBuild(); if (previousRun != null && previousRun.isBuilding()) { listener.getLogger().println(Messages.ExtendedEmailPublisher__is_still_in_progress_ignoring_for_purpo(previousRun.getDisplayName())); return null; } else { return previousRun; } } @Override public ExtendedEmailPublisherDescriptor getDescriptor() { return (ExtendedEmailPublisherDescriptor) Jenkins.getActiveInstance().getDescriptor(getClass()); } public static ExtendedEmailPublisherDescriptor descriptor() { return Jenkins.getActiveInstance().getDescriptorByType(ExtendedEmailPublisherDescriptor.class); } public MatrixAggregator createAggregator(MatrixBuild matrixbuild, Launcher launcher, BuildListener buildlistener) { return new MatrixAggregator(matrixbuild, launcher, buildlistener) { @Override public boolean endBuild() throws InterruptedException, IOException { LOGGER.log(Level.FINER, "end build of {0}", this.build.getDisplayName()); // Will be run by parent so we check if needed to be executed by parent if (getMatrixTriggerMode().forParent) { return ExtendedEmailPublisher.this._perform(this.build, this.launcher, this.listener, false); } return true; } @Override public boolean startBuild() throws InterruptedException, IOException { LOGGER.log(Level.FINER, "end build of {0}", this.build.getDisplayName()); // Will be run by parent so we check if needed to be executed by parent if (getMatrixTriggerMode().forParent) { return ExtendedEmailPublisher.this._perform(this.build, this.launcher, this.listener, true); } return true; } }; } public Object readResolve() { if (Jenkins.getActiveInstance().isUseSecurity() && (!StringUtils.isBlank(this.postsendScript) || !StringUtils.isBlank(this.presendScript))) { setPostsendScript(this.postsendScript); setPresendScript(this.presendScript); setClasspath(this.classpath); } return this; } }