/*******************************************************************************
*
* Copyright (c) 2004-2011 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Bruce Chapman, Erik Ramfelt, Jean-Baptiste Quenot, Luca Domenico Milanesio, Anton Kozak
*
*
*******************************************************************************/
package hudson.tasks;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import hudson.EnvVars;
import hudson.Extension;
import hudson.Functions;
import hudson.Launcher;
import hudson.RestrictedSince;
import hudson.Util;
import hudson.diagnosis.OldDataMonitor;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Hudson;
import hudson.model.User;
import hudson.model.UserPropertyDescriptor;
import hudson.util.FormValidation;
import hudson.util.Secret;
import hudson.util.XStream2;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Properties;
import java.util.logging.Logger;
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 net.sf.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.export.Exported;
import static hudson.Util.fixEmptyAndTrim;
/**
* {@link Publisher} that sends the build result in e-mail.
*
* @author Kohsuke Kawaguchi
*/
public class Mailer extends Notifier {
protected static final Logger LOGGER = Logger.getLogger(Mailer.class.getName());
/**
* Whitespace-separated list of e-mail addresses that represent recipients.
*/
//TODO: review and check whether we can do it private
public String recipients;
/**
* If true, only the first unstable build will be reported.
*/
//TODO: review and check whether we can do it private
public boolean dontNotifyEveryUnstableBuild;
/**
* If true, individuals will receive e-mails regarding who broke the build.
*/
//TODO: review and check whether we can do it private
public boolean sendToIndividuals;
public String getRecipients() {
return recipients;
}
public boolean isDontNotifyEveryUnstableBuild() {
return dontNotifyEveryUnstableBuild;
}
public boolean isSendToIndividuals() {
return sendToIndividuals;
}
// TODO: left so that XStream won't get angry. figure out how to set the error handling behavior
// in XStream. Deprecated since 2005-04-23.
private transient String from;
private transient String subject;
private transient boolean failureOnly;
private transient String charset;
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener)
throws IOException, InterruptedException {
if (debug) {
listener.getLogger().println("Running mailer");
}
// substitute build parameters
EnvVars env = build.getEnvironment(listener);
String recip = env.expand(recipients);
return new MailSender(recip, dontNotifyEveryUnstableBuild, sendToIndividuals,
descriptor().getCharset()).execute(build, listener);
}
/**
* This class does explicit check pointing.
*/
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
/**
* @deprecated as of 1.286 Use {@link #descriptor()} to obtain the current
* instance.
*/
@Deprecated
@RestrictedSince("1.355")
public static DescriptorImpl DESCRIPTOR;
public static DescriptorImpl descriptor() {
return Hudson.getInstance().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.
*/
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.
*/
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;
/**
* The charset to use for the text and subject.
*/
private String charset;
/**
* Used to keep track of number test e-mails.
*/
private static transient int testEmailCount = 0;
public DescriptorImpl() {
load();
DESCRIPTOR = this;
}
public String getDisplayName() {
return Messages.Mailer_DisplayName();
}
@Override
public String getHelpFile() {
return "/help/project-config/mailer.html";
}
public String getDefaultSuffix() {
return defaultSuffix;
}
/**
* 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());
props.put("mail.transport.protocol", "smtp");
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 work around 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"));
setAdminAddress(json.getString("adminAddress"));
defaultSuffix = nullify(json.getString("defaultSuffix"));
String url = nullify(json.getString("url"));
if (url != null && !url.endsWith("/")) {
url += '/';
}
hudsonUrl = url;
if (json.has("useSMTPAuth")) {
JSONObject auth = json.getJSONObject("useSMTPAuth");
smtpAuthUsername = nullify(auth.getString("smtpAuthUserName"));
smtpAuthPassword = Secret.fromString(nullify(auth.getString("smtpAuthPassword")));
} 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;
}
public String getAdminAddress() {
String v = adminAddress;
if (v == null) {
v = Messages.Mailer_Address_Not_Configured();
}
return v;
}
public String getUrl() {
return hudsonUrl;
}
public String getSmtpAuthUserName() {
return smtpAuthUsername;
}
public String getSmtpAuthPassword() {
if (smtpAuthPassword == null) {
return null;
}
return Secret.toString(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;
}
public void setHudsonUrl(String hudsonUrl) {
this.hudsonUrl = hudsonUrl;
}
public void setAdminAddress(String adminAddress) {
if (adminAddress.startsWith("\"") && adminAddress.endsWith("\"")) {
// some users apparently quote the whole thing. Don't konw why
// anyone does this, but it's a machine's job to forgive human mistake
adminAddress = adminAddress.substring(1, adminAddress.length() - 1);
}
this.adminAddress = 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) {
Mailer m = new Mailer();
req.bindParameters(m, "mailer_");
m.dontNotifyEveryUnstableBuild = req.getParameter("mailer_notifyEveryUnstableBuild") == null;
if (hudsonUrl == null) {
// if Hudson URL is not configured yet, infer some default
hudsonUrl = Functions.inferHudsonURL(req);
save();
}
return m;
}
/**
* Checks the URL in <tt>global.jelly</tt>
*/
public FormValidation doCheckUrl(@QueryParameter String value) {
if (value.startsWith("http://localhost")) {
return FormValidation.warning(Messages.Mailer_Localhost_Error());
}
return FormValidation.ok();
}
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 doCheckAdminAddress(@QueryParameter String value) {
return doAddressCheck(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 String smtpAuthPassword,
@QueryParameter boolean useSsl, @QueryParameter String smtpPort) throws IOException, ServletException, InterruptedException {
try {
if (!useSMTPAuth) {
smtpAuthUserName = smtpAuthPassword = null;
}
Session session = createSession(smtpServer, smtpPort, useSsl, smtpAuthUserName,
Secret.fromString(smtpAuthPassword));
MimeMessage msg = new HudsonMimeMessage(session);
msg.setSubject("Test email #" + ++testEmailCount);
msg.setContent("This is test email #" + testEmailCount + " sent from Hudson Continuous Integration server.", "text/plain");
msg.setFrom(new InternetAddress(adminAddress));
msg.setSentDate(new Date());
msg.setRecipient(Message.RecipientType.TO, new InternetAddress(adminAddress));
//See http://issues.hudson-ci.org/browse/HUDSON-7426 and
//http://www.oracle.com/technetwork/java/faq-135477.html#smtpauth
send(smtpServer, smtpAuthUserName, smtpAuthPassword, smtpPort, (HudsonMimeMessage) msg);
return FormValidation.ok("Email was successfully sent");
} catch (MessagingException e) {
return FormValidation.errorWithMarkup("<p>Failed to send out e-mail</p><pre>" + Util.escape(Functions.printThrowable(e)) + "</pre>");
}
}
/**
* Sends message
*
* @param msg {@link MimeMessage}
* @throws MessagingException if any.
*/
public void send(HudsonMimeMessage msg) throws MessagingException {
send(smtpHost, smtpAuthUsername, Secret.toString(smtpAuthPassword), smtpPort, msg);
}
/**
* Wrap {@link Transport#send(javax.mail.Message)} method. Based on <a
* href="http://www.oracle.com/technetwork/java/faq-135477.html#smtpauth">javax.mail
* recommendations</a> and fix <a
* href="http://issues.hudson-ci.org/browse/HUDSON-7426">HUDSON-7426</a>
*
* @param smtpServer smtp server
* @param smtpAuthUserName username
* @param smtpAuthPassword password.
* @param smtpPort port.
* @param msg {@link MimeMessage}
* @throws MessagingException if any.
* @see
* {@link #createSession(String, String, boolean, String, hudson.util.Secret)}
*/
public static void send(String smtpServer, String smtpAuthUserName, String smtpAuthPassword, String smtpPort,
HudsonMimeMessage msg) throws MessagingException {
if (null != msg && null != msg.getSession()) {
Session session = msg.getSession();
Transport t = null != session.getProperty("mail.transport.protocol")
? session.getTransport() : session.getTransport("smtp");
smtpPort = fixEmptyAndTrim(smtpPort);
int port = -1;
if (StringUtils.isNumeric(smtpPort)) {
port = Integer.parseInt(smtpPort);
}
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=399631
if (smtpAuthPassword != null && smtpAuthPassword.trim().length() == 0) {
smtpAuthPassword = null;
}
t.connect(smtpServer, port, smtpAuthUserName, smtpAuthPassword);
msg.saveChanges();
t.sendMessage(msg, msg.getAllRecipients());
t.close();
}
}
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 (emailAddress != null) {
return emailAddress;
}
// try the inference logic
return MailAddressResolver.resolve(user);
}
@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(StaplerRequest req, JSONObject formData) throws FormException {
return new UserProperty(req.getParameter("email.address"));
}
}
}
/**
* Debug probe point to be activated by the scripting console.
*/
public static boolean debug = false;
public static class ConverterImpl extends XStream2.PassthruConverter<Mailer> {
public ConverterImpl(XStream2 xstream) {
super(xstream);
}
@Override
protected void callback(Mailer m, UnmarshallingContext context) {
if (m.from != null || m.subject != null || m.failureOnly || m.charset != null) {
OldDataMonitor.report(context, "1.10");
}
}
}
}