package hudson.plugins.emailext;
import hudson.EnvVars;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Hudson;
import hudson.model.Result;
import hudson.model.User;
import hudson.plugins.emailext.plugins.ContentBuilder;
import hudson.plugins.emailext.plugins.EmailTrigger;
import hudson.plugins.emailext.plugins.EmailTriggerDescriptor;
import hudson.scm.ChangeLogSet.Entry;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.MailMessageIdAction;
import hudson.tasks.Mailer;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import hudson.util.FormValidation;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
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 java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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.
*
* @author kyle.sweeney@valtech.com
*
*/
public class ExtendedEmailPublisher extends Notifier {
private static final Logger LOGGER = Logger.getLogger(Mailer.class.getName());
public static final String COMMA_SEPARATED_SPLIT_REGEXP = "[,\\s]+";
private static final Map<String,EmailTriggerDescriptor> EMAIL_TRIGGER_TYPE_MAP = new HashMap<String,EmailTriggerDescriptor>();
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 PROJECT_DEFAULT_SUBJECT_TEXT = "$PROJECT_DEFAULT_SUBJECT";
public static final String PROJECT_DEFAULT_BODY_TEXT = "$PROJECT_DEFAULT_CONTENT";
public static final String CHARSET = "utf-8";
public static void addEmailTriggerType(EmailTriggerDescriptor triggerType) throws EmailExtException {
if(EMAIL_TRIGGER_TYPE_MAP.containsKey(triggerType.getMailerId()))
throw new EmailExtException("An email trigger type with name " +
triggerType.getTriggerName() + " was already added.");
EMAIL_TRIGGER_TYPE_MAP.put(triggerType.getMailerId(), triggerType);
}
public static void removeEmailTriggerType(EmailTriggerDescriptor triggerType) {
if(EMAIL_TRIGGER_TYPE_MAP.containsKey(triggerType.getMailerId()))
EMAIL_TRIGGER_TYPE_MAP.remove(triggerType.getMailerId());
}
public static EmailTriggerDescriptor getEmailTriggerType(String mailerId) {
return EMAIL_TRIGGER_TYPE_MAP.get(mailerId);
}
public static Collection<EmailTriggerDescriptor> getEmailTriggers() {
return EMAIL_TRIGGER_TYPE_MAP.values();
}
public static Collection<String> getEmailTriggerNames() {
return EMAIL_TRIGGER_TYPE_MAP.keySet();
}
public static List<EmailTrigger> getTriggersForNonConfiguredInstance() {
List<EmailTrigger> retList = new ArrayList<EmailTrigger>();
for(String mailerId : EMAIL_TRIGGER_TYPE_MAP.keySet()) {
retList.add(EMAIL_TRIGGER_TYPE_MAP.get(mailerId).getNewInstance(null));
}
return retList;
}
/**
* A comma-separated list of email recipient that will be used for every trigger.
*/
public String recipientList;
/** This is the list of email triggers that the project has configured */
private List<EmailTrigger> configuredTriggers = new ArrayList<EmailTrigger>();
/**
* 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;
/**
* Get the list of configured email triggers for this project.
*/
public List<EmailTrigger> getConfiguredTriggers() {
if(configuredTriggers == null)
configuredTriggers = new ArrayList<EmailTrigger>();
return configuredTriggers;
}
/**
* Get the list of non-configured email triggers for this project.
*/
public List<EmailTrigger> getNonConfiguredTriggers() {
List<EmailTrigger> confTriggers = getConfiguredTriggers();
List<EmailTrigger> retList = new ArrayList<EmailTrigger>();
for(String mailerId : EMAIL_TRIGGER_TYPE_MAP.keySet()) {
boolean contains = false;
for(EmailTrigger trigger : confTriggers) {
if(trigger.getDescriptor().getMailerId().equals(mailerId)) {
contains = true;
break;
}
}
if(!contains) {
retList.add(EMAIL_TRIGGER_TYPE_MAP.get(mailerId).getNewInstance(null));
}
}
return retList;
}
/**
* Return true if the project has been configured, otherwise returns false
*/
public boolean isConfigured() {
return !getConfiguredTriggers().isEmpty();
}
/**
* Return true if the project has been configured, otherwise returns false
*/
public boolean getConfigured() {
return isConfigured();
}
@Override
public boolean prebuild(AbstractBuild<?,?> build, BuildListener listener) {
return _perform(build,listener,true);
}
@Override
public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
return _perform(build,listener,false);
}
private boolean _perform(AbstractBuild<?,?> build, BuildListener listener, boolean forPreBuild) {
boolean emailTriggered = false;
Map<String,EmailTrigger> triggered = new HashMap<String, EmailTrigger>();
for(EmailTrigger trigger : configuredTriggers) {
if(trigger.isPreBuild() == forPreBuild && trigger.trigger(build)) {
String tName = trigger.getDescriptor().getTriggerName();
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<String>();
for(String triggerName : triggered.keySet()) {
replacedTriggers.addAll(triggered.get(triggerName).getDescriptor().getTriggerReplaceList());
}
for(String triggerName : replacedTriggers) {
triggered.remove(triggerName);
listener.getLogger().println("Trigger " + triggerName + " was overridden by another trigger and will not send an email.");
}
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()) {
listener.getLogger().println("Sending email for trigger: " + triggerName);
sendMail(triggered.get(triggerName).getEmail(), build, listener);
}
return true;
}
private boolean sendMail(EmailType mailType, AbstractBuild<?,?> build, BuildListener listener) {
try {
MimeMessage msg = createMail(mailType, build, listener);
Address[] allRecipients = msg.getAllRecipients();
if (allRecipients != null) {
StringBuilder buf = new StringBuilder("Sending email to:");
for (Address a : allRecipients)
buf.append(' ').append(a);
listener.getLogger().println(buf);
Transport.send(msg);
if (build.getAction(MailMessageIdAction.class) == null)
build.addAction(new MailMessageIdAction(msg.getMessageID()));
return true;
} else {
listener.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(listener.error("Could not send email as a part of the post-build publishers."));
}
return false;
}
private MimeMessage createMail(EmailType type, AbstractBuild<?,?> build, BuildListener listener) throws MessagingException, IOException, InterruptedException {
boolean overrideGlobalSettings = ExtendedEmailPublisher.DESCRIPTOR.getOverrideGlobalSettings();
MimeMessage msg;
// If not overriding global settings, use the Mailer class to create a session and set the from address
// Else we'll do it ourselves
if (!overrideGlobalSettings) {
msg = new MimeMessage(Mailer.descriptor().createSession());
msg.setFrom(new InternetAddress(Mailer.descriptor().getAdminAddress()));
} else {
msg = new MimeMessage(ExtendedEmailPublisher.DESCRIPTOR.createSession());
msg.setFrom(new InternetAddress(ExtendedEmailPublisher.DESCRIPTOR.getAdminAddress()));
}
// Set the contents of the email
msg.setSentDate(new Date());
setSubject( type, build, msg );
setContent( type, build, msg );
EnvVars env = build.getEnvironment(listener);
// Get the recipients from the global list of addresses
List<InternetAddress> recipientAddresses = new ArrayList<InternetAddress>();
if (type.getSendToRecipientList()) {
for (String recipient : env.expand(recipientList).split(COMMA_SEPARATED_SPLIT_REGEXP)) {
addAddress(recipientAddresses, recipient, listener);
}
}
// Get the list of developers who made changes between this build and the last
// if this mail type is configured that way
if (type.getSendToDevelopers()) {
Set<User> users;
if (type.getIncludeCulprits()) {
users = build.getCulprits();
} else {
users = new HashSet<User>();
for (Entry change : build.getChangeSet()) {
users.add(change.getAuthor());
}
}
for (User user : users) {
String adrs = user.getProperty(Mailer.UserProperty.class).getAddress();
if (adrs != null)
addAddress(recipientAddresses, adrs, listener);
else {
listener.getLogger().println("Failed to send e-mail to " + user.getFullName() + " because no e-mail address is known, and no default e-mail domain is configured");
}
}
}
//Get the list of recipients that are uniquely specified for this type of email
if (type.getRecipientList() != null && type.getRecipientList().trim().length() > 0) {
String[] typeRecipients = env.expand(type.getRecipientList()).split(COMMA_SEPARATED_SPLIT_REGEXP);
for (int i = 0; i < typeRecipients.length; i++) {
recipientAddresses.add(new InternetAddress(typeRecipients[i]));
}
}
msg.setRecipients(Message.RecipientType.TO, recipientAddresses.toArray(new InternetAddress[recipientAddresses.size()]));
AbstractBuild<?,?> pb = build.getPreviousBuild();
if (pb!=null) {
// Send mails as replies until next successful build
MailMessageIdAction b = pb.getAction(MailMessageIdAction.class);
if(b!=null && pb.getResult()!=Result.SUCCESS) {
msg.setHeader("In-Reply-To",b.messageId);
msg.setHeader("References",b.messageId);
}
}
return msg;
}
private void setSubject( final EmailType type, final AbstractBuild<?, ?> build, MimeMessage msg )
throws MessagingException
{
String subject = new ContentBuilder().transformText(type.getSubject(), this, type, build);
msg.setSubject(subject, CHARSET);
}
private void setContent( final EmailType type, final AbstractBuild<?, ?> build, MimeMessage msg )
throws MessagingException
{
final String text = new ContentBuilder().transformText(type.getBody(), this, type, build);
String messageContentType = contentType;
// contentType is null if the project was not reconfigured after upgrading.
if (messageContentType == null || "default".equals(messageContentType)) {
messageContentType = DESCRIPTOR.getDefaultContentType();
// The defaultContentType is null if the main Hudson configuration
// was not reconfigured after upgrading.
if (messageContentType == null) {
messageContentType = "text/plain";
}
}
messageContentType += "; charset=" + CHARSET;
msg.setContent(text, messageContentType);
}
private static void addAddress(List<InternetAddress> addresses, String address, BuildListener listener) {
try {
addresses.add(new InternetAddress(address));
} catch(AddressException ae) {
LOGGER.log(Level.WARNING, "Could not create email address.", ae);
listener.getLogger().println("Failed to create e-mail address for " + address);
}
}
@Override
public boolean needsToRunAfterFinalized() {
return true;
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.BUILD;
}
@Override
public BuildStepDescriptor<Publisher> getDescriptor() {
return DESCRIPTOR;
}
@Extension
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
/*
* These settings are the settings that are global.
*/
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.
*/
private String hudsonUrl;
/**
* If non-null, use SMTP-AUTH with these information.
*/
private String smtpAuthPassword,smtpAuthUsername;
/**
* The e-mail address that Hudson puts to "From:" field in outgoing e-mails.
* Null if not configured.
*/
private String adminAddress;
/**
* 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;
/**
* This is a global default content type (mime type) for emails.
*/
private String defaultContentType;
/**
* This is a global default subject line for sending emails.
*/
private String defaultSubject;
/**
* This is a global default body for sending emails.
*/
private String defaultBody;
private boolean overrideGlobalSettings;
@Override
public String getDisplayName() {
return "Editable Email Notification";
}
public String getAdminAddress() {
String v = adminAddress;
if (v == null) {
v = "address not configured yet <nobody>";
}
return v;
}
public String getDefaultSuffix() {
return defaultSuffix;
}
/** JavaMail session. */
public Session createSession() {
/*
* Mailer.DescriptorImpl desc = Mailer.descriptor();
smtpHost = nullify(desc.getSmtpServer());
adminAddress = desc.getAdminAddress();
defaultSuffix = nullify(desc.getDefaultSuffix());
hudsonUrl = desc.getUrl();
smtpAuthUsername = desc.getSmtpAuthUserName();
smtpAuthPassword = desc.getSmtpAuthPassword();
useSsl = desc.getUseSsl();
smtpPort = desc.getSmtpPort();
*/
Properties props = new Properties(System.getProperties());
if(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");
return Session.getInstance(props,getAuthenticator());
}
private Authenticator getAuthenticator() {
final String un = getSmtpAuthUsername();
if (un == null) return null;
return new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(getSmtpAuthUsername(), getSmtpAuthPassword());
}
};
}
public String getHudsonUrl() {
if (hudsonUrl == null) {
return Hudson.getInstance().getRootUrl();
}
return hudsonUrl;
}
public String getSmtpServer() {
return smtpHost;
}
public String getSmtpAuthUsername() {
return smtpAuthUsername;
}
public String getSmtpAuthPassword() {
return smtpAuthPassword;
}
public boolean getUseSsl() {
return useSsl;
}
public String getSmtpPort() {
return smtpPort;
}
public String getDefaultContentType() {
return defaultContentType;
}
public String getDefaultSubject() {
return defaultSubject;
}
public String getDefaultBody() {
return defaultBody;
}
public boolean getOverrideGlobalSettings() {
return overrideGlobalSettings;
}
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true;
}
@Override
public Publisher newInstance(StaplerRequest req, JSONObject formData) throws hudson.model.Descriptor.FormException {
// Save the recipient lists
String listRecipients = formData.getString("recipientlist_recipients");
// Save configuration for each trigger type
ExtendedEmailPublisher m = new ExtendedEmailPublisher();
m.recipientList = listRecipients;
m.contentType = formData.getString("project_content_type");
m.defaultSubject = formData.getString("project_default_subject");
m.defaultContent = formData.getString("project_default_content");
m.configuredTriggers = new ArrayList<EmailTrigger>();
// Create a new email trigger for each one that is configured
for (String mailerId : EMAIL_TRIGGER_TYPE_MAP.keySet()) {
if("true".equalsIgnoreCase(formData.optString("mailer_" + mailerId + "_configured"))) {
EmailType type = createMailType(formData, mailerId);
EmailTrigger trigger = EMAIL_TRIGGER_TYPE_MAP.get(mailerId).getNewInstance(type);
m.configuredTriggers.add(trigger);
}
}
return m;
}
private EmailType createMailType(JSONObject formData, String mailType) {
EmailType m = new EmailType();
String prefix = "mailer_" + mailType + '_';
m.setSubject(formData.getString(prefix + "subject"));
m.setBody(formData.getString(prefix + "body"));
m.setRecipientList(formData.getString(prefix + "recipientList"));
m.setSendToRecipientList(formData.optBoolean(prefix + "sendToRecipientList"));
m.setSendToDevelopers(formData.optBoolean(prefix + "sendToDevelopers"));
m.setIncludeCulprits(formData.optBoolean(prefix + "includeCulprits"));
return m;
}
public DescriptorImpl() {
super(ExtendedEmailPublisher.class);
load();
if (defaultBody == null && defaultSubject == null) {
defaultBody = DEFAULT_BODY_TEXT;
defaultSubject = DEFAULT_SUBJECT_TEXT;
}
}
@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
// Most of this stuff is the same as the built-in email publisher
// Configure the smtp server
smtpHost = nullify(req.getParameter("ext_mailer_smtp_server"));
adminAddress = req.getParameter("ext_mailer_admin_address");
defaultSuffix = nullify(req.getParameter("ext_mailer_default_suffix"));
// Specify the url to this hudson instance
String url = nullify(req.getParameter("ext_mailer_hudson_url"));
if (url != null && !url.endsWith("/")) {
url += '/';
}
if (url == null) {
url = Hudson.getInstance().getRootUrl();
}
hudsonUrl = url;
// specify authentication information
if (req.getParameter("extmailer.useSMTPAuth") != null) {
smtpAuthUsername = nullify(req.getParameter("extmailer.SMTPAuth.userName"));
smtpAuthPassword = nullify(req.getParameter("extmailer.SMTPAuth.password"));
} else {
smtpAuthUsername = smtpAuthPassword = null;
}
// specify if the mail server uses ssl for authentication
useSsl = req.getParameter("ext_mailer_smtp_use_ssl") != null;
// specify custom smtp port
smtpPort = nullify(req.getParameter("ext_mailer_smtp_port"));
defaultContentType = nullify(req.getParameter("ext_mailer_default_content_type"));
// Allow global defaults to be set for the subject and body of the email
defaultSubject = nullify(req.getParameter("ext_mailer_default_subject"));
defaultBody = nullify(req.getParameter("ext_mailer_default_body"));
overrideGlobalSettings = req.getParameter("ext_mailer_override_global_settings") != null;
save();
return super.configure(req, formData);
}
private String nullify(String v) {
if(v!=null && v.length()==0)
v=null;
return v;
}
@Override
public String getHelpFile() {
return "/plugin/email-ext/help/main.html";
}
public FormValidation doAddressCheck(
@QueryParameter final String value) throws IOException, ServletException {
try {
new InternetAddress(value);
return FormValidation.ok();
} catch (AddressException e) {
return FormValidation.error(e.getMessage());
}
}
public FormValidation doRecipientListRecipientsCheck(
@QueryParameter final String value) throws IOException, ServletException {
if(value != null && value.trim().length() > 0) {
String[] names = value.split(COMMA_SEPARATED_SPLIT_REGEXP);
try {
for(int i=0;i<names.length;i++) {
if(names[i].trim().length()>0) {
new InternetAddress(names[i]);
}
}
}
catch(AddressException e) {
return FormValidation.error(e.getMessage());
}
}
return FormValidation.ok();
}
}
}