/* * The MIT License * * Copyright (c) 2004-2010, 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 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.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 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. */ @Restricted(NoExternalUse.class) @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 = "http://localhost:8080/"; /** * 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 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")); 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); } 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"); } } }