PageRenderTime 62ms CodeModel.GetById 3ms app.highlight 52ms RepoModel.GetById 2ms app.codeStats 0ms

/hudson-core/src/main/java/hudson/tasks/Mailer.java

http://github.com/hudson/hudson
Java | 565 lines | 351 code | 80 blank | 134 comment | 47 complexity | e6734a54093f5a6b657dd61131011a1f MD5 | raw file
  1/*
  2 * The MIT License
  3 * 
  4 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
  5 * Bruce Chapman, Erik Ramfelt, Jean-Baptiste Quenot, Luca Domenico Milanesio
  6 * 
  7 * Permission is hereby granted, free of charge, to any person obtaining a copy
  8 * of this software and associated documentation files (the "Software"), to deal
  9 * in the Software without restriction, including without limitation the rights
 10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 11 * copies of the Software, and to permit persons to whom the Software is
 12 * furnished to do so, subject to the following conditions:
 13 * 
 14 * The above copyright notice and this permission notice shall be included in
 15 * all copies or substantial portions of the Software.
 16 * 
 17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 23 * THE SOFTWARE.
 24 */
 25package hudson.tasks;
 26
 27import com.thoughtworks.xstream.converters.UnmarshallingContext;
 28import hudson.EnvVars;
 29import hudson.Extension;
 30import hudson.Functions;
 31import hudson.Launcher;
 32import hudson.RestrictedSince;
 33import hudson.Util;
 34import hudson.diagnosis.OldDataMonitor;
 35import hudson.model.AbstractBuild;
 36import hudson.model.AbstractProject;
 37import hudson.model.BuildListener;
 38import hudson.model.Hudson;
 39import hudson.model.User;
 40import hudson.model.UserPropertyDescriptor;
 41import hudson.util.FormValidation;
 42import hudson.util.Secret;
 43import hudson.util.XStream2;
 44import java.io.IOException;
 45import java.net.InetAddress;
 46import java.net.UnknownHostException;
 47import java.util.Date;
 48import java.util.Properties;
 49import java.util.logging.Logger;
 50import javax.mail.Authenticator;
 51import javax.mail.Message;
 52import javax.mail.MessagingException;
 53import javax.mail.PasswordAuthentication;
 54import javax.mail.Session;
 55import javax.mail.Transport;
 56import javax.mail.internet.AddressException;
 57import javax.mail.internet.InternetAddress;
 58import javax.mail.internet.MimeMessage;
 59import javax.servlet.ServletException;
 60import net.sf.json.JSONObject;
 61import org.apache.commons.lang3.StringUtils;
 62import org.kohsuke.accmod.Restricted;
 63import org.kohsuke.accmod.restrictions.NoExternalUse;
 64import org.kohsuke.stapler.QueryParameter;
 65import org.kohsuke.stapler.StaplerRequest;
 66import org.kohsuke.stapler.export.Exported;
 67
 68import static hudson.Util.fixEmptyAndTrim;
 69
 70/**
 71 * {@link Publisher} that sends the build result in e-mail.
 72 *
 73 * @author Kohsuke Kawaguchi
 74 */
 75public class Mailer extends Notifier {
 76    protected static final Logger LOGGER = Logger.getLogger(Mailer.class.getName());
 77
 78    /**
 79     * Whitespace-separated list of e-mail addresses that represent recipients.
 80     */
 81    //TODO: review and check whether we can do it private
 82    public String recipients;
 83
 84    /**
 85     * If true, only the first unstable build will be reported.
 86     */
 87    //TODO: review and check whether we can do it private
 88    public boolean dontNotifyEveryUnstableBuild;
 89
 90    /**
 91     * If true, individuals will receive e-mails regarding who broke the build.
 92     */
 93    //TODO: review and check whether we can do it private
 94    public boolean sendToIndividuals;
 95
 96    public String getRecipients() {
 97        return recipients;
 98    }
 99
100    public boolean isDontNotifyEveryUnstableBuild() {
101        return dontNotifyEveryUnstableBuild;
102    }
103
104    public boolean isSendToIndividuals() {
105        return sendToIndividuals;
106    }
107
108    // TODO: left so that XStream won't get angry. figure out how to set the error handling behavior
109    // in XStream.  Deprecated since 2005-04-23.
110    private transient String from;
111    private transient String subject;
112    private transient boolean failureOnly;
113    private transient String charset;
114
115    @Override
116    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener)
117        throws IOException, InterruptedException {
118        if (debug) {
119            listener.getLogger().println("Running mailer");
120        }
121        // substitute build parameters
122        EnvVars env = build.getEnvironment(listener);
123        String recip = env.expand(recipients);
124
125        return new MailSender(recip, dontNotifyEveryUnstableBuild, sendToIndividuals,
126            descriptor().getCharset()).execute(build, listener);
127    }
128
129    /**
130     * This class does explicit check pointing.
131     */
132    public BuildStepMonitor getRequiredMonitorService() {
133        return BuildStepMonitor.NONE;
134    }
135
136    /**
137     * @deprecated as of 1.286
138     *      Use {@link #descriptor()} to obtain the current instance.
139     */
140    @Restricted(NoExternalUse.class)
141    @RestrictedSince("1.355")
142    public static DescriptorImpl DESCRIPTOR;
143
144    public static DescriptorImpl descriptor() {
145        return Hudson.getInstance().getDescriptorByType(Mailer.DescriptorImpl.class);
146    }
147
148    @Extension
149    public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
150        /**
151         * The default e-mail address suffix appended to the user name found from changelog,
152         * to send e-mails. Null if not configured.
153         */
154        private String defaultSuffix;
155
156        /**
157         * Hudson's own URL, to put into the e-mail.
158         */
159        private String hudsonUrl = "http://localhost:8080/";
160
161        /**
162         * If non-null, use SMTP-AUTH with these information.
163         */
164        private String smtpAuthUsername;
165
166        private Secret smtpAuthPassword;
167
168        /**
169         * The e-mail address that Hudson puts to "From:" field in outgoing e-mails.
170         * Null if not configured.
171         */
172        private String adminAddress;
173
174        /**
175         * The SMTP server to use for sending e-mail. Null for default to the environment,
176         * which is usually <tt>localhost</tt>.
177         */
178        private String smtpHost;
179
180        /**
181         * If true use SSL on port 465 (standard SMTPS) unless <code>smtpPort</code> is set.
182         */
183        private boolean useSsl;
184
185        /**
186         * The SMTP port to use for sending e-mail. Null for default to the environment,
187         * which is usually <tt>25</tt>.
188         */
189        private String smtpPort;
190
191        /**
192         * The charset to use for the text and subject.
193         */
194        private String charset;
195
196        /**
197         * Used to keep track of number test e-mails.
198         */
199        private static transient int testEmailCount = 0;
200
201
202        public DescriptorImpl() {
203            load();
204            DESCRIPTOR = this;
205        }
206
207        public String getDisplayName() {
208            return Messages.Mailer_DisplayName();
209        }
210
211        @Override
212        public String getHelpFile() {
213            return "/help/project-config/mailer.html";
214        }
215
216        public String getDefaultSuffix() {
217            return defaultSuffix;
218        }
219
220        /** JavaMail session. */
221        public Session createSession() {
222            return createSession(smtpHost,smtpPort,useSsl,smtpAuthUsername,smtpAuthPassword);
223        }
224        private static Session createSession(String smtpHost, String smtpPort, boolean useSsl, String smtpAuthUserName, Secret smtpAuthPassword) {
225            smtpPort = fixEmptyAndTrim(smtpPort);
226            smtpAuthUserName = fixEmptyAndTrim(smtpAuthUserName);
227
228            Properties props = new Properties(System.getProperties());
229            props.put("mail.transport.protocol", "smtp");
230            if(fixEmptyAndTrim(smtpHost)!=null)
231                props.put("mail.smtp.host",smtpHost);
232            if (smtpPort!=null) {
233                props.put("mail.smtp.port", smtpPort);
234            }
235            if (useSsl) {
236            	/* This allows the user to override settings by setting system properties but
237            	 * also allows us to use the default SMTPs port of 465 if no port is already set.
238            	 * It would be cleaner to use smtps, but that's done by calling session.getTransport()...
239            	 * and thats done in mail sender, and it would be a bit of a hack to get it all to
240            	 * coordinate, and we can make it work through setting mail.smtp properties.
241            	 */
242            	if (props.getProperty("mail.smtp.socketFactory.port") == null) {
243                    String port = smtpPort==null?"465":smtpPort;
244                    props.put("mail.smtp.port", port);
245                    props.put("mail.smtp.socketFactory.port", port);
246            	}
247            	if (props.getProperty("mail.smtp.socketFactory.class") == null) {
248            		props.put("mail.smtp.socketFactory.class","javax.net.ssl.SSLSocketFactory");
249            	}
250				props.put("mail.smtp.socketFactory.fallback", "false");
251			}
252            if(smtpAuthUserName!=null)
253                props.put("mail.smtp.auth","true");
254
255            // avoid hang by setting some timeout. 
256            props.put("mail.smtp.timeout","60000");
257            props.put("mail.smtp.connectiontimeout","60000");
258
259            return Session.getInstance(props,getAuthenticator(smtpAuthUserName,Secret.toString(smtpAuthPassword)));
260        }
261
262        private static Authenticator getAuthenticator(final String smtpAuthUserName, final String smtpAuthPassword) {
263            if(smtpAuthUserName==null)    return null;
264            return new Authenticator() {
265                @Override
266                protected PasswordAuthentication getPasswordAuthentication() {
267                    return new PasswordAuthentication(smtpAuthUserName,smtpAuthPassword);
268                }
269            };
270        }
271
272        @Override
273        public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
274            // this code is brain dead
275            smtpHost = nullify(json.getString("smtpServer"));
276            setAdminAddress(json.getString("adminAddress"));
277
278            defaultSuffix = nullify(json.getString("defaultSuffix"));
279            String url = nullify(json.getString("url"));
280            if(url!=null && !url.endsWith("/"))
281                url += '/';
282            hudsonUrl = url;
283
284            if(json.has("useSMTPAuth")) {
285                JSONObject auth = json.getJSONObject("useSMTPAuth");
286                smtpAuthUsername = nullify(auth.getString("smtpAuthUserName"));
287                smtpAuthPassword = Secret.fromString(nullify(auth.getString("smtpAuthPassword")));
288            } else {
289                smtpAuthUsername = null;
290                smtpAuthPassword = null;
291            }
292            smtpPort = nullify(json.getString("smtpPort"));
293            useSsl = json.getBoolean("useSsl");
294            charset = json.getString("charset");
295            if (charset == null || charset.length() == 0)
296            	charset = "UTF-8";
297
298            save();
299            return true;
300        }
301
302        private String nullify(String v) {
303            if(v!=null && v.length()==0)    v=null;
304            return v;
305        }
306
307        public String getSmtpServer() {
308            return smtpHost;
309        }
310
311        public String getAdminAddress() {
312            String v = adminAddress;
313            if(v==null)     v = Messages.Mailer_Address_Not_Configured();
314            return v;
315        }
316
317        public String getUrl() {
318            return hudsonUrl;
319        }
320
321        public String getSmtpAuthUserName() {
322            return smtpAuthUsername;
323        }
324
325        public String getSmtpAuthPassword() {
326            if (smtpAuthPassword==null) return null;
327            return Secret.toString(smtpAuthPassword);
328        }
329
330        public boolean getUseSsl() {
331        	return useSsl;
332        }
333
334        public String getSmtpPort() {
335        	return smtpPort;
336        }
337
338        public String getCharset() {
339        	String c = charset;
340        	if (c == null || c.length() == 0)	c = "UTF-8";
341        	return c;
342        }
343
344        public void setDefaultSuffix(String defaultSuffix) {
345            this.defaultSuffix = defaultSuffix;
346        }
347
348        public void setHudsonUrl(String hudsonUrl) {
349            this.hudsonUrl = hudsonUrl;
350        }
351
352        public void setAdminAddress(String adminAddress) {
353            if(adminAddress.startsWith("\"") && adminAddress.endsWith("\"")) {
354                // some users apparently quote the whole thing. Don't konw why
355                // anyone does this, but it's a machine's job to forgive human mistake
356                adminAddress = adminAddress.substring(1,adminAddress.length()-1);
357            }
358            this.adminAddress = adminAddress;
359        }
360
361        public void setSmtpHost(String smtpHost) {
362            this.smtpHost = smtpHost;
363        }
364
365        public void setUseSsl(boolean useSsl) {
366            this.useSsl = useSsl;
367        }
368
369        public void setSmtpPort(String smtpPort) {
370            this.smtpPort = smtpPort;
371        }
372
373        public void setCharset(String chaset) {
374            this.charset = chaset;
375        }
376
377        public void setSmtpAuth(String userName, String password) {
378            this.smtpAuthUsername = userName;
379            this.smtpAuthPassword = Secret.fromString(password);
380        }
381
382        @Override
383        public Publisher newInstance(StaplerRequest req, JSONObject formData) {
384            Mailer m = new Mailer();
385            req.bindParameters(m,"mailer_");
386            m.dontNotifyEveryUnstableBuild = req.getParameter("mailer_notifyEveryUnstableBuild")==null;
387
388            if(hudsonUrl==null) {
389                // if Hudson URL is not configured yet, infer some default
390                hudsonUrl = Functions.inferHudsonURL(req);
391                save();
392            }
393
394            return m;
395        }
396
397        /**
398         * Checks the URL in <tt>global.jelly</tt>
399         */
400        public FormValidation doCheckUrl(@QueryParameter String value) {
401            if(value.startsWith("http://localhost"))
402                return FormValidation.warning(Messages.Mailer_Localhost_Error());
403            return FormValidation.ok();
404        }
405
406        public FormValidation doAddressCheck(@QueryParameter String value) {
407            try {
408                new InternetAddress(value);
409                return FormValidation.ok();
410            } catch (AddressException e) {
411                return FormValidation.error(e.getMessage());
412            }
413        }
414
415        public FormValidation doCheckSmtpServer(@QueryParameter String value) {
416            try {
417                if (fixEmptyAndTrim(value)!=null)
418                    InetAddress.getByName(value);
419                return FormValidation.ok();
420            } catch (UnknownHostException e) {
421                return FormValidation.error(Messages.Mailer_Unknown_Host_Name()+value);
422            }
423        }
424
425        public FormValidation doCheckAdminAddress(@QueryParameter String value) {
426            return doAddressCheck(value);
427        }
428
429        public FormValidation doCheckDefaultSuffix(@QueryParameter String value) {
430            if (value.matches("@[A-Za-z0-9.\\-]+") || fixEmptyAndTrim(value)==null)
431                return FormValidation.ok();
432            else
433                return FormValidation.error(Messages.Mailer_Suffix_Error());
434        }
435
436        /**
437         * Send an email to the admin address
438         * @throws IOException
439         * @throws ServletException
440         * @throws InterruptedException
441         */
442        public FormValidation doSendTestMail(
443                @QueryParameter String smtpServer, @QueryParameter String adminAddress, @QueryParameter boolean useSMTPAuth,
444                @QueryParameter String smtpAuthUserName, @QueryParameter String smtpAuthPassword,
445                @QueryParameter boolean useSsl, @QueryParameter String smtpPort) throws IOException, ServletException, InterruptedException {
446            try {
447                if (!useSMTPAuth)   smtpAuthUserName = smtpAuthPassword = null;
448
449                Session session = createSession(smtpServer, smtpPort, useSsl, smtpAuthUserName,
450                    Secret.fromString(smtpAuthPassword));
451                MimeMessage msg = new HudsonMimeMessage(session);
452                msg.setSubject("Test email #" + ++testEmailCount);
453                msg.setContent("This is test email #" + testEmailCount + " sent from Hudson Continuous Integration server.", "text/plain");
454                msg.setFrom(new InternetAddress(adminAddress));
455                msg.setSentDate(new Date());
456                msg.setRecipient(Message.RecipientType.TO, new InternetAddress(adminAddress));
457
458                //See http://issues.hudson-ci.org/browse/HUDSON-7426 and
459                //http://www.oracle.com/technetwork/java/faq-135477.html#smtpauth
460                send(smtpServer, smtpAuthUserName, smtpAuthPassword, smtpPort, (HudsonMimeMessage) msg);
461
462                return FormValidation.ok("Email was successfully sent");
463            } catch (MessagingException e) {
464                return FormValidation.errorWithMarkup("<p>Failed to send out e-mail</p><pre>"+Util.escape(Functions.printThrowable(e))+"</pre>");
465            }
466        }
467
468        /**
469         * Sends message
470         * @param msg {@link MimeMessage}
471         * @throws MessagingException if any.
472         */
473        public void send(HudsonMimeMessage msg) throws MessagingException {
474            send(smtpHost, smtpAuthUsername, Secret.toString(smtpAuthPassword), smtpPort, msg);
475        }
476
477        /**
478         * Wrap {@link Transport#send(javax.mail.Message)} method. Based on
479         * <a href="http://www.oracle.com/technetwork/java/faq-135477.html#smtpauth">javax.mail recommendations</a>
480         * and fix <a href="http://issues.hudson-ci.org/browse/HUDSON-7426">HUDSON-7426</a>
481         *
482         * @param smtpServer smtp server
483         * @param smtpAuthUserName username
484         * @param smtpAuthPassword password.
485         * @param smtpPort port.
486         * @param msg {@link MimeMessage}
487         * @throws MessagingException if any.
488         * @see {@link #createSession(String, String, boolean, String, hudson.util.Secret)}
489         */
490        public static void send(String smtpServer, String smtpAuthUserName, String smtpAuthPassword, String smtpPort,
491                                HudsonMimeMessage msg) throws MessagingException {
492            if (null != msg && null !=msg.getSession()) {
493                Session session = msg.getSession();
494                Transport t = null != session.getProperty("mail.transport.protocol") ?
495                    session.getTransport() : session.getTransport("smtp");
496                smtpPort = fixEmptyAndTrim(smtpPort);
497                int port = -1;
498                if (StringUtils.isNumeric(smtpPort)) {
499                    port = Integer.parseInt(smtpPort);
500                }
501                t.connect(smtpServer, port, smtpAuthUserName, smtpAuthPassword);
502                msg.saveChanges();
503                t.sendMessage(msg, msg.getAllRecipients());
504                t.close();
505            }
506        }
507
508        public boolean isApplicable(Class<? extends AbstractProject> jobType) {
509            return true;
510        }
511    }
512
513    /**
514     * Per user property that is e-mail address.
515     */
516    public static class UserProperty extends hudson.model.UserProperty {
517        /**
518         * The user's e-mail address.
519         * Null to leave it to default.
520         */
521        private final String emailAddress;
522
523        public UserProperty(String emailAddress) {
524            this.emailAddress = emailAddress;
525        }
526
527        @Exported
528        public String getAddress() {
529            if(emailAddress!=null)
530                return emailAddress;
531
532            // try the inference logic
533            return MailAddressResolver.resolve(user);
534        }
535
536        @Extension
537        public static final class DescriptorImpl extends UserPropertyDescriptor {
538            public String getDisplayName() {
539                return Messages.Mailer_UserProperty_DisplayName();
540            }
541
542            public UserProperty newInstance(User user) {
543                return new UserProperty(null);
544            }
545
546            @Override
547            public UserProperty newInstance(StaplerRequest req, JSONObject formData) throws FormException {
548                return new UserProperty(req.getParameter("email.address"));
549            }
550        }
551    }
552
553    /**
554     * Debug probe point to be activated by the scripting console.
555     */
556    public static boolean debug = false;
557
558    public static class ConverterImpl extends XStream2.PassthruConverter<Mailer> {
559        public ConverterImpl(XStream2 xstream) { super(xstream); }
560        @Override protected void callback(Mailer m, UnmarshallingContext context) {
561            if (m.from != null || m.subject != null || m.failureOnly || m.charset != null)
562                OldDataMonitor.report(context, "1.10");
563        }
564    }
565}