Skip to content

Commit

Permalink
Add post-send script feature
Browse files Browse the repository at this point in the history
For some tasks pre-send scripts are not sufficient, like rewriting the
Message-ID based on the SMTP response for later In-Reply-To headers.
This is needed when using Amazon Simple Email Service (AWS SES) for
sending emails.

Other possible use cases are scripts that act upon failed sending
attempts.

Here is a example post-send script that does the message id rewriting
for AWS SES:

```groovy
import com.sun.mail.smtp.SMTPTransport;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import hudson.plugins.emailext.ExtendedEmailPublisherDescriptor;

String smtpHost = props.getProperty("mail.smtp.host", "");
String awsRegex = "^email-smtp\\.([a-z0-9-]+)\\.amazonaws\\.com\$";
Pattern p = Pattern.compile(awsRegex);
Matcher m = p.matcher(smtpHost);
if (m.matches()) {
    String region = m.group(1);
    if (transport instanceof SMTPTransport) {
        String response = ((SMTPTransport)transport).getLastServerResponse();
        String[] parts = response.trim().split(" +");
        if (parts.length == 3 && parts[0].equals("250") && parts[1].equals("Ok")) {
            String MessageID = "<" + parts[2] + "@" + region + ".amazonses.com>";
            msg.setHeader("Message-ID", MessageID);
        }
    }
}
```

References:
- https://issues.jenkins-ci.org/browse/JENKINS-30854
- #108
- http://docs.aws.amazon.com/ses/latest/DeveloperGuide/smtp-response-codes.html
  • Loading branch information
weisslj committed Oct 17, 2015
1 parent e6a743d commit 8c0c532
Show file tree
Hide file tree
Showing 20 changed files with 329 additions and 12 deletions.
80 changes: 76 additions & 4 deletions src/main/java/hudson/plugins/emailext/ExtendedEmailPublisher.java
Expand Up @@ -113,6 +113,11 @@ public class ExtendedEmailPublisher extends Notifier implements MatrixAggregatab
*/
public String presendScript;

/**
* The project's post-send script.
*/
public String postsendScript;

public List<GroovyScriptPath> classpath;

/**
Expand Down Expand Up @@ -147,18 +152,18 @@ public class ExtendedEmailPublisher extends Notifier implements MatrixAggregatab

@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,
String project_default_content, String project_attachments, String project_presend_script, String project_postsend_script,
int project_attach_buildlog, String project_replyto, 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_attachments, project_presend_script, project_postsend_script, project_attach_buildlog, project_replyto,
project_save_output, project_triggers, matrixTriggerMode, false, Collections.EMPTY_LIST);
}

@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,
String project_default_content, String project_attachments, String project_presend_script, String project_postsend_script,
int project_attach_buildlog, String project_replyto, boolean project_save_output,
List<EmailTrigger> project_triggers, MatrixTriggerMode matrixTriggerMode, boolean project_disabled,
List<GroovyScriptPath> classpath) {
Expand All @@ -168,6 +173,7 @@ public ExtendedEmailPublisher(String project_recipient_list, String project_cont
this.defaultContent = project_default_content;
this.attachmentsPattern = project_attachments;
this.presendScript = project_presend_script;
this.postsendScript = project_postsend_script;
this.attachBuildLog = project_attach_buildlog > 0;
this.compressBuildLog = project_attach_buildlog > 1;
this.replyTo = project_replyto;
Expand Down Expand Up @@ -350,15 +356,23 @@ private boolean sendMail(ExtendedEmailPublisherContext context) {
}
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.send(msg);
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();
Expand Down Expand Up @@ -392,6 +406,7 @@ private boolean sendMail(ExtendedEmailPublisherContext context) {
} 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());
Expand All @@ -404,6 +419,11 @@ private boolean sendMail(ExtendedEmailPublisherContext context) {
break;
}
}

executePostsendScript(context, msg, session, transport);
// close transport after post-send script, so server response can be accessed:
transport.close();

if (context.getBuild().getAction(MailMessageIdAction.class) == null) {
context.getBuild().addAction(new MailMessageIdAction(msg.getMessageID()));
}
Expand Down Expand Up @@ -487,6 +507,58 @@ private boolean executePresendScript(ExtendedEmailPublisherContext context, Mime
return !cancel;
}

private void executePostsendScript(ExtendedEmailPublisherContext context, MimeMessage msg, Session session, Transport transport)
throws RuntimeException {
String script = ContentBuilder.transformText(postsendScript, context, getRuntimeMacros(context));
if (StringUtils.isNotBlank(script)) {
debug(context.getListener().getLogger(), "Executing post-send script");
ClassLoader cl = Jenkins.getInstance().getPluginManager().uberClassLoader;
ScriptSandbox sandbox = null;
CompilerConfiguration cc = new CompilerConfiguration();
cc.setScriptBaseClass(EmailExtScript.class.getCanonicalName());
cc.addCompilationCustomizers(new ImportCustomizer().addStarImports(
"jenkins",
"jenkins.model",
"hudson",
"hudson.model"));

expandClasspath(context, cc);
if (getDescriptor().isSecurityEnabled()) {
debug(context.getListener().getLogger(), "Setting up sandbox for post-send script");
cc.addCompilationCustomizers(new SandboxTransformer());
sandbox = new ScriptSandbox();
}

Binding binding = new Binding();
binding.setVariable("build", context.getBuild());
binding.setVariable("msg", msg);
binding.setVariable("props", session.getProperties());
binding.setVariable("transport", transport);
binding.setVariable("listener", context.getListener());
binding.setVariable("logger", context.getListener().getLogger());
binding.setVariable("trigger", context.getTrigger());
binding.setVariable("triggered", ImmutableMultimap.copyOf(context.getTriggered()));

GroovyShell shell = new GroovyShell(cl, binding, cc);
StringWriter out = new StringWriter();
PrintWriter pw = new PrintWriter(out);

if (sandbox != null) {
sandbox.register();
}

try {
shell.evaluate(script);
} catch (SecurityException e) {
context.getListener().getLogger().println("Post-send script tried to access secured objects: " + e.getMessage());
} catch (Throwable t) {
t.printStackTrace(pw);
context.getListener().getLogger().println(out.toString());
}
debug(context.getListener().getLogger(), out.toString());
}
}

/**
* Expand the plugin class loader with URL taken from the project descriptor
* and the global configuration.
Expand Down
Expand Up @@ -99,6 +99,11 @@ public final class ExtendedEmailPublisherDescriptor extends BuildStepDescriptor<
*/
private String defaultPresendScript = "";

/**
* This is the global default post-send script.
*/
private String defaultPostsendScript = "";

private List<GroovyScriptPath> defaultClasspath = new ArrayList<GroovyScriptPath>();

private transient List<EmailTriggerDescriptor> defaultTriggers = new ArrayList<EmailTriggerDescriptor>();
Expand Down Expand Up @@ -328,6 +333,10 @@ public String getDefaultPresendScript() {
return defaultPresendScript;
}

public String getDefaultPostsendScript() {
return defaultPostsendScript;
}

public List<GroovyScriptPath> getDefaultClasspath() {
return defaultClasspath;
}
Expand Down Expand Up @@ -397,6 +406,8 @@ public boolean configure(StaplerRequest req, JSONObject formData)
? req.getParameter("ext_mailer_default_replyto") : "";
defaultPresendScript = nullify(req.getParameter("ext_mailer_default_presend_script")) != null
? req.getParameter("ext_mailer_default_presend_script") : "";
defaultPostsendScript = nullify(req.getParameter("ext_mailer_default_postsend_script")) != null
? req.getParameter("ext_mailer_default_postsend_script") : "";
if (req.hasParameter("ext_mailer_default_classpath")) {
defaultClasspath.clear();
for (String s : req.getParameterValues("ext_mailer_default_classpath")) {
Expand Down
Expand Up @@ -35,6 +35,7 @@ public class ContentBuilder {
private static final String DEFAULT_RECIPIENTS = "\\$DEFAULT_RECIPIENTS|\\$\\{DEFAULT_RECIPIENTS\\}";
private static final String DEFAULT_REPLYTO = "\\$DEFAULT_REPLYTO|\\$\\{DEFAULT_REPLYTO\\}";
private static final String DEFAULT_PRESEND_SCRIPT = "\\$DEFAULT_PRESEND_SCRIPT|\\$\\{DEFAULT_PRESEND_SCRIPT\\}";
private static final String DEFAULT_POSTSEND_SCRIPT = "\\$DEFAULT_POSTSEND_SCRIPT|\\$\\{DEFAULT_POSTSEND_SCRIPT\\}";
private static final String PROJECT_DEFAULT_BODY = "\\$PROJECT_DEFAULT_CONTENT|\\$\\{PROJECT_DEFAULT_CONTENT\\}";
private static final String PROJECT_DEFAULT_SUBJECT = "\\$PROJECT_DEFAULT_SUBJECT|\\$\\{PROJECT_DEFAULT_SUBJECT\\}";
private static final String PROJECT_DEFAULT_REPLYTO = "\\$PROJECT_DEFAULT_REPLYTO|\\$\\{PROJECT_DEFAULT_REPLYTO\\}";
Expand All @@ -54,6 +55,7 @@ public static String transformText(String origText, ExtendedEmailPublisherContex
String defaultRecipients = Matcher.quoteReplacement(noNull(context.getPublisher().getDescriptor().getDefaultRecipients()));
String defaultExtReplyTo = Matcher.quoteReplacement(noNull(context.getPublisher().getDescriptor().getDefaultReplyTo()));
String defaultPresendScript = Matcher.quoteReplacement(noNull(context.getPublisher().getDescriptor().getDefaultPresendScript()));
String defaultPostsendScript = Matcher.quoteReplacement(noNull(context.getPublisher().getDescriptor().getDefaultPostsendScript()));
String newText = origText.replaceAll(
PROJECT_DEFAULT_BODY, defaultContent).replaceAll(
PROJECT_DEFAULT_SUBJECT, defaultSubject).replaceAll(
Expand All @@ -62,7 +64,8 @@ public static String transformText(String origText, ExtendedEmailPublisherContex
DEFAULT_SUBJECT, defaultExtSubject).replaceAll(
DEFAULT_RECIPIENTS, defaultRecipients).replaceAll(
DEFAULT_REPLYTO, defaultExtReplyTo).replaceAll(
DEFAULT_PRESEND_SCRIPT, defaultPresendScript);
DEFAULT_PRESEND_SCRIPT, defaultPresendScript).replaceAll(
DEFAULT_POSTSEND_SCRIPT, defaultPostsendScript);

try {
List<TokenMacro> macros = new ArrayList<TokenMacro>(getPrivateMacros());
Expand Down
Expand Up @@ -59,6 +59,9 @@ f.advanced(title: _("Advanced Settings")) {
f.entry(title: _("Pre-send Script"), help: "/plugin/email-ext/help/projectConfig/presendScript.html") {
f.textarea(id: "project_presend_script", name: "project_presend_script", value: configured ? instance.presendScript : "\$DEFAULT_PRESEND_SCRIPT", class: "setting-input")
}
f.entry(title: _("Post-send Script"), help: "/plugin/email-ext/help/projectConfig/postsendScript.html") {
f.textarea(id: "project_postsend_script", name: "project_postsend_script", value: configured ? instance.postsendScript : "\$DEFAULT_POSTSEND_SCRIPT", class: "setting-input")
}
f.entry(title: _("Additional groovy classpath"), help: "/plugin/help/projectConfig/defaultClasspath.html") {
f.repeatable(field: "classpath") {
f.textbox(field: "path")
Expand Down
Expand Up @@ -72,6 +72,9 @@ f.section(title: _("Extended E-mail Notification")) {
f.entry(help: "/plugin/email-ext/help/globalConfig/defaultPresendScript.html", title: _("Default Pre-send Script")) {
f.textarea(class: "setting-input", value: descriptor.defaultPresendScript, name: "ext_mailer_default_presend_script")
}
f.entry(help: "/plugin/email-ext/help/globalConfig/defaultPostsendScript.html", title: _("Default Post-send Script")) {
f.textarea(class: "setting-input", value: descriptor.defaultPostsendScript, name: "ext_mailer_default_postsend_script")
}
f.entry(title: _("Additional groovy classpath"), help: "/plugin/email-ext/help/globalConfig/defaultClasspath.html") {
f.repeatable(field: "defaultClasspath") {
f.textbox(field: "path", name: "ext_mailer_default_classpath")
Expand Down
Expand Up @@ -18,6 +18,9 @@ dl() {
dt("\${DEFAULT_PRESEND_SCRIPT}")
dd(_("defaultPresend"))

dt("\${DEFAULT_POSTSEND_SCRIPT}")
dd(_("defaultPostsend"))

dt("\${PROJECT_DEFAULT_SUBJECT}")
dd(_("projectDefaultSubject"))

Expand Down
Expand Up @@ -6,6 +6,8 @@ defaultSubject=This is the default email subject that is configured in Jenkins's
defaultContent=This is the default email content that is configured in Jenkins's system configuration page.
defaultPresend=This is the default pre-send script content that is configured in Jenkins's system configuration. \
This is the only token supported in the pre-send script entry field.
defaultPostsend=This is the default post-send script content that is configured in Jenkins's system configuration. \
This is the only token supported in the post-send script entry field.
projectDefaultSubject=This is the default email subject for this project. The result of using this token in \
the advanced configuration is what is in the Default Subject field above. WARNING: Do not use this token in \
the Default Subject or Content fields. Doing this has an undefined result.
Expand Down
@@ -1,13 +1,13 @@
Jive Formatter
==================

jive-formatter.groovy contains methods for easy and convenient formatting of emails being sent from Jenkins to Jive. It should be called from the Pre-send Script area.
jive-formatter.groovy contains methods for easy and convenient formatting of emails being sent from Jenkins to Jive. It should be called from the Pre-send or Post-send Script area.

Also, it doesn't seem like Jive supports text with multiple formats, so only call one formatting method per block of text.

Either formatLine or formatText can and should be called on every line of text that will be sent to the Jive system prior to calling formatting methods like color or size. Please test on your own instances of Jive and add functionality as you find it!

The following lines should be added to the Pre-send Script area prior to attempting to invoke any functions.
The following lines should be added to the Pre-send or Post-send Script area prior to attempting to invoke any functions.

File sourceFile = new File("/your/preferred/path/jive-formatter.groovy");
Class groovyClass = new GroovyClassLoader(getClass().getClassLoader()).parseClass(sourceFile);
Expand Down
Expand Up @@ -2,12 +2,12 @@ import java.lang.reflect.Array;
import java.util.regex.*
/**
* This is a Groovy class to allow easy formatting for Jive social networking systems.
* It was designed to work in the email-ext plugin for Jenkins. It should be called from the Pre-send Script area.
* It was designed to work in the email-ext plugin for Jenkins. It should be called from the Pre-send or Post-send Script area.
* Also, it doesn't seem like Jive supports text with multiple formats, so only call one formatting method per block of text.
* Either formatLine or formatText can and should be called on every line of text that will be sent to the Jive system
* prior to calling formatting methods like color or size.
* <p>
* The following lines should be added to the Pre-send Script area prior to attempting to invoke any functions.
* The following lines should be added to the Pre-send or Post-send Script area prior to attempting to invoke any functions.
* <p>
* File sourceFile = new File("/your/preferred/path/jive-formatter.groovy");
* Class groovyClass = new GroovyClassLoader(getClass().getClassLoader()).parseClass(sourceFile);
Expand Down
2 changes: 1 addition & 1 deletion src/main/webapp/help/globalConfig/defaultClasspath.html
@@ -1,6 +1,6 @@
<div>
These pathes allow to extend the Groovy classpath when the
presend script is run. The syntax of the path is either a
presend and postsend script are run. The syntax of the path is either a
full url or a simple file path (may be relative).
These pathes are common to all projects when configured at
the global scope.
Expand Down
13 changes: 13 additions & 0 deletions src/main/webapp/help/globalConfig/defaultPostsendScript.html
@@ -0,0 +1,13 @@
<div>
This script will be run after sending the email to allow
modifying the email after sending. The MimeMessage variable
is "msg," the SMTPTransport "transport," the session
properties "props," the build is also available as "build"
and a logger is available as "logger." The trigger that
caused the email is available as "trigger" and all triggered
builds are available as a map "triggered."
<br/><br/>
You can set the default post-send script here and then use
${DEFAULT_POSTSEND_SCRIPT} in the project settings to use
the script written here.
</div>
2 changes: 1 addition & 1 deletion src/main/webapp/help/globalConfig/security.html
@@ -1,6 +1,6 @@
<div>
<p>
When enabled, it removes the ability for pre-send scripts to directly access the Jenkins instance. A security exception will be thrown
When enabled, it removes the ability for pre-send and post-send scripts to directly access the Jenkins instance. A security exception will be thrown
if the user tries to access the Jenkins instance of the manager object.
</p>
</div>
2 changes: 1 addition & 1 deletion src/main/webapp/help/projectConfig/defaultClasspath.html
@@ -1,6 +1,6 @@
<div>
These pathes allow to extend the Groovy classpath when the
presend script is run. The syntax of the path is either a
presend and postsend script are run. The syntax of the path is either a
full url or a simple file path (may be relative).
These pathes are specific to this project and will be added
to those of the global scope.
Expand Down
7 changes: 7 additions & 0 deletions src/main/webapp/help/projectConfig/postsendScript.html
@@ -0,0 +1,7 @@
<div>
This script will be run after sending the email to allow
acting upon the send result. The MimeMessage variable
is "msg," the SMTPTransport "transport," the session
properties "props," the build is also available as "build"
and a logger is available as "logger."
</div>

0 comments on commit 8c0c532

Please sign in to comment.