4
* Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi,
5
* Bruce Chapman, Erik Ramfelt, Jean-Baptiste Quenot, Luca Domenico Milanesio
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:
14
* The above copyright notice and this permission notice shall be included in
15
* all copies or substantial portions of the Software.
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
27
import static hudson.Util.fixEmptyAndTrim;
29
import hudson.EnvVars;
30
import hudson.Extension;
31
import hudson.Functions;
32
import hudson.Launcher;
33
import hudson.RestrictedSince;
35
import hudson.model.AbstractBuild;
36
import hudson.model.AbstractProject;
37
import hudson.model.BuildListener;
38
import hudson.model.User;
39
import hudson.model.UserPropertyDescriptor;
40
import hudson.util.FormValidation;
41
import hudson.util.Secret;
43
import org.apache.commons.lang.StringUtils;
44
import org.apache.tools.ant.types.selectors.SelectorUtils;
45
import org.kohsuke.accmod.Restricted;
46
import org.kohsuke.accmod.restrictions.NoExternalUse;
47
import org.kohsuke.stapler.QueryParameter;
48
import org.kohsuke.stapler.StaplerRequest;
49
import org.kohsuke.stapler.export.Exported;
52
import java.io.IOException;
53
import java.io.UnsupportedEncodingException;
54
import java.net.InetAddress;
55
import java.net.UnknownHostException;
56
import java.util.Date;
57
import java.util.Properties;
58
import java.util.logging.Level;
59
import java.util.logging.Logger;
60
import java.util.regex.Pattern;
61
import java.util.regex.Matcher;
62
import javax.mail.Address;
63
import javax.mail.Authenticator;
64
import javax.mail.Message;
65
import javax.mail.MessagingException;
66
import javax.mail.PasswordAuthentication;
67
import javax.mail.Session;
68
import javax.mail.Transport;
69
import javax.mail.internet.AddressException;
70
import javax.mail.internet.InternetAddress;
71
import javax.mail.internet.MimeMessage;
72
import javax.servlet.ServletException;
74
import org.apache.tools.ant.types.selectors.SelectorUtils;
75
import org.kohsuke.accmod.Restricted;
76
import org.kohsuke.accmod.restrictions.NoExternalUse;
77
import org.kohsuke.stapler.QueryParameter;
78
import org.kohsuke.stapler.StaplerRequest;
79
import org.kohsuke.stapler.export.Exported;
81
import jenkins.model.Jenkins;
82
import net.sf.json.JSONObject;
85
* {@link Publisher} that sends the build result in e-mail.
87
* @author Kohsuke Kawaguchi
89
public class Mailer extends Notifier {
90
protected static final Logger LOGGER = Logger.getLogger(Mailer.class.getName());
93
* Whitespace-separated list of e-mail addresses that represent recipients.
95
public String recipients;
98
* If true, only the first unstable build will be reported.
100
public boolean dontNotifyEveryUnstableBuild;
103
* If true, individuals will receive e-mails regarding who broke the build.
105
public boolean sendToIndividuals;
108
public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
110
listener.getLogger().println("Running mailer");
111
// substitute build parameters
112
EnvVars env = build.getEnvironment(listener);
113
String recip = env.expand(recipients);
115
return new MailSender(recip, dontNotifyEveryUnstableBuild, sendToIndividuals, descriptor().getCharset()) {
116
/** Check whether a path (/-separated) will be archived. */
118
public boolean artifactMatches(String path, AbstractBuild<?,?> build) {
119
ArtifactArchiver aa = build.getProject().getPublishersList().get(ArtifactArchiver.class);
121
LOGGER.finer("No ArtifactArchiver found");
124
String artifacts = aa.getArtifacts();
125
for (String include : artifacts.split("[, ]+")) {
126
String pattern = include.replace(File.separatorChar, '/');
127
if (pattern.endsWith("/")) {
130
if (SelectorUtils.matchPath(pattern, path)) {
131
LOGGER.log(Level.FINER, "DescriptorImpl.artifactMatches true for {0} against {1}", new Object[] {path, pattern});
135
LOGGER.log(Level.FINER, "DescriptorImpl.artifactMatches for {0} matched none of {1}", new Object[] {path, artifacts});
138
}.execute(build,listener);
142
* This class does explicit check pointing.
144
public BuildStepMonitor getRequiredMonitorService() {
145
return BuildStepMonitor.NONE;
148
private static Pattern ADDRESS_PATTERN = Pattern.compile("\\s*([^<]*)<([^>]+)>\\s*");
149
public static InternetAddress StringToAddress(String strAddress, String charset) throws AddressException, UnsupportedEncodingException {
150
Matcher m = ADDRESS_PATTERN.matcher(strAddress);
152
return new InternetAddress(strAddress);
155
String personal = m.group(1);
156
String address = m.group(2);
157
return new InternetAddress(address, personal, charset);
161
* @deprecated as of 1.286
162
* Use {@link #descriptor()} to obtain the current instance.
164
@Restricted(NoExternalUse.class)
165
@RestrictedSince("1.355")
166
public static DescriptorImpl DESCRIPTOR;
168
public static DescriptorImpl descriptor() {
169
return Jenkins.getInstance().getDescriptorByType(Mailer.DescriptorImpl.class);
173
public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
175
* The default e-mail address suffix appended to the user name found from changelog,
176
* to send e-mails. Null if not configured.
178
private String defaultSuffix;
181
* Hudson's own URL, to put into the e-mail.
183
private String hudsonUrl;
186
* If non-null, use SMTP-AUTH with these information.
188
private String smtpAuthUsername;
190
private Secret smtpAuthPassword;
193
* The e-mail address that Hudson puts to "From:" field in outgoing e-mails.
194
* Null if not configured.
196
private String adminAddress;
199
* The e-mail address that Jenkins puts to "Reply-To" header in outgoing e-mails.
200
* Null if not configured.
202
private String replyToAddress;
205
* The SMTP server to use for sending e-mail. Null for default to the environment,
206
* which is usually <tt>localhost</tt>.
208
private String smtpHost;
211
* If true use SSL on port 465 (standard SMTPS) unless <code>smtpPort</code> is set.
213
private boolean useSsl;
216
* The SMTP port to use for sending e-mail. Null for default to the environment,
217
* which is usually <tt>25</tt>.
219
private String smtpPort;
222
* The charset to use for the text and subject.
224
private String charset;
227
* Used to keep track of number test e-mails.
229
private static transient int testEmailCount = 0;
232
public DescriptorImpl() {
237
public String getDisplayName() {
238
return Messages.Mailer_DisplayName();
242
public String getHelpFile() {
243
return "/help/project-config/mailer.html";
246
public String getDefaultSuffix() {
247
return defaultSuffix;
250
public String getReplyToAddress() {
251
return replyToAddress;
254
public void setReplyToAddress(String address) {
255
this.replyToAddress = Util.fixEmpty(address);
258
/** JavaMail session. */
259
public Session createSession() {
260
return createSession(smtpHost,smtpPort,useSsl,smtpAuthUsername,smtpAuthPassword);
262
private static Session createSession(String smtpHost, String smtpPort, boolean useSsl, String smtpAuthUserName, Secret smtpAuthPassword) {
263
smtpPort = fixEmptyAndTrim(smtpPort);
264
smtpAuthUserName = fixEmptyAndTrim(smtpAuthUserName);
266
Properties props = new Properties(System.getProperties());
267
if(fixEmptyAndTrim(smtpHost)!=null)
268
props.put("mail.smtp.host",smtpHost);
269
if (smtpPort!=null) {
270
props.put("mail.smtp.port", smtpPort);
273
/* This allows the user to override settings by setting system properties but
274
* also allows us to use the default SMTPs port of 465 if no port is already set.
275
* It would be cleaner to use smtps, but that's done by calling session.getTransport()...
276
* and thats done in mail sender, and it would be a bit of a hack to get it all to
277
* coordinate, and we can make it work through setting mail.smtp properties.
279
if (props.getProperty("mail.smtp.socketFactory.port") == null) {
280
String port = smtpPort==null?"465":smtpPort;
281
props.put("mail.smtp.port", port);
282
props.put("mail.smtp.socketFactory.port", port);
284
if (props.getProperty("mail.smtp.socketFactory.class") == null) {
285
props.put("mail.smtp.socketFactory.class","javax.net.ssl.SSLSocketFactory");
287
props.put("mail.smtp.socketFactory.fallback", "false");
289
if(smtpAuthUserName!=null)
290
props.put("mail.smtp.auth","true");
292
// avoid hang by setting some timeout.
293
props.put("mail.smtp.timeout","60000");
294
props.put("mail.smtp.connectiontimeout","60000");
296
return Session.getInstance(props,getAuthenticator(smtpAuthUserName,Secret.toString(smtpAuthPassword)));
299
private static Authenticator getAuthenticator(final String smtpAuthUserName, final String smtpAuthPassword) {
300
if(smtpAuthUserName==null) return null;
301
return new Authenticator() {
303
protected PasswordAuthentication getPasswordAuthentication() {
304
return new PasswordAuthentication(smtpAuthUserName,smtpAuthPassword);
310
public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
311
// this code is brain dead
312
smtpHost = nullify(json.getString("smtpServer"));
313
setAdminAddress(json.getString("adminAddress"));
314
setReplyToAddress(json.getString("replyToAddress"));
316
defaultSuffix = nullify(json.getString("defaultSuffix"));
317
String url = nullify(json.getString("url"));
318
if(url!=null && !url.endsWith("/"))
322
if(json.has("useSMTPAuth")) {
323
JSONObject auth = json.getJSONObject("useSMTPAuth");
324
smtpAuthUsername = nullify(auth.getString("smtpAuthUserName"));
325
smtpAuthPassword = Secret.fromString(nullify(auth.getString("smtpAuthPassword")));
327
smtpAuthUsername = null;
328
smtpAuthPassword = null;
330
smtpPort = nullify(json.getString("smtpPort"));
331
useSsl = json.getBoolean("useSsl");
332
charset = json.getString("charset");
333
if (charset == null || charset.length() == 0)
340
private String nullify(String v) {
341
if(v!=null && v.length()==0) v=null;
345
public String getSmtpServer() {
349
public String getAdminAddress() {
350
String v = adminAddress;
351
if(v==null) v = Messages.Mailer_Address_Not_Configured();
355
public String getUrl() {
359
public String getSmtpAuthUserName() {
360
return smtpAuthUsername;
363
public String getSmtpAuthPassword() {
364
if (smtpAuthPassword==null) return null;
365
return Secret.toString(smtpAuthPassword);
368
public boolean getUseSsl() {
372
public String getSmtpPort() {
376
public String getCharset() {
378
if (c == null || c.length() == 0) c = "UTF-8";
382
public void setDefaultSuffix(String defaultSuffix) {
383
this.defaultSuffix = defaultSuffix;
386
public void setHudsonUrl(String hudsonUrl) {
387
this.hudsonUrl = hudsonUrl;
390
public void setAdminAddress(String adminAddress) {
391
if(adminAddress.startsWith("\"") && adminAddress.endsWith("\"")) {
392
// some users apparently quote the whole thing. Don't konw why
393
// anyone does this, but it's a machine's job to forgive human mistake
394
adminAddress = adminAddress.substring(1,adminAddress.length()-1);
396
this.adminAddress = adminAddress;
399
public void setSmtpHost(String smtpHost) {
400
this.smtpHost = smtpHost;
403
public void setUseSsl(boolean useSsl) {
404
this.useSsl = useSsl;
407
public void setSmtpPort(String smtpPort) {
408
this.smtpPort = smtpPort;
411
public void setCharset(String chaset) {
412
this.charset = chaset;
415
public void setSmtpAuth(String userName, String password) {
416
this.smtpAuthUsername = userName;
417
this.smtpAuthPassword = Secret.fromString(password);
421
public Publisher newInstance(StaplerRequest req, JSONObject formData) {
422
Mailer m = new Mailer();
423
req.bindParameters(m,"mailer_");
424
m.dontNotifyEveryUnstableBuild = req.getParameter("mailer_notifyEveryUnstableBuild")==null;
426
if(hudsonUrl==null) {
427
// if Hudson URL is not configured yet, infer some default
428
hudsonUrl = Functions.inferHudsonURL(req);
436
* Checks the URL in <tt>global.jelly</tt>
438
public FormValidation doCheckUrl(@QueryParameter String value) {
439
if(value.startsWith("http://localhost"))
440
return FormValidation.warning(Messages.Mailer_Localhost_Error());
441
return FormValidation.ok();
444
public FormValidation doAddressCheck(@QueryParameter String value) {
446
new InternetAddress(value);
447
return FormValidation.ok();
448
} catch (AddressException e) {
449
return FormValidation.error(e.getMessage());
453
public FormValidation doCheckSmtpServer(@QueryParameter String value) {
455
if (fixEmptyAndTrim(value)!=null)
456
InetAddress.getByName(value);
457
return FormValidation.ok();
458
} catch (UnknownHostException e) {
459
return FormValidation.error(Messages.Mailer_Unknown_Host_Name()+value);
463
public FormValidation doCheckAdminAddress(@QueryParameter String value) {
464
return doAddressCheck(value);
467
public FormValidation doCheckDefaultSuffix(@QueryParameter String value) {
468
if (value.matches("@[A-Za-z0-9.\\-]+") || fixEmptyAndTrim(value)==null)
469
return FormValidation.ok();
471
return FormValidation.error(Messages.Mailer_Suffix_Error());
475
* Send an email to the admin address
476
* @throws IOException
477
* @throws ServletException
478
* @throws InterruptedException
480
public FormValidation doSendTestMail(
481
@QueryParameter String smtpServer, @QueryParameter String adminAddress, @QueryParameter boolean useSMTPAuth,
482
@QueryParameter String smtpAuthUserName, @QueryParameter String smtpAuthPassword,
483
@QueryParameter boolean useSsl, @QueryParameter String smtpPort, @QueryParameter String charset,
484
@QueryParameter String sendTestMailTo) throws IOException, ServletException, InterruptedException {
486
if (!useSMTPAuth) smtpAuthUserName = smtpAuthPassword = null;
488
MimeMessage msg = new MimeMessage(createSession(smtpServer,smtpPort,useSsl,smtpAuthUserName,Secret.fromString(smtpAuthPassword)));
489
msg.setSubject(Messages.Mailer_TestMail_Subject(++testEmailCount), charset);
490
msg.setText(Messages.Mailer_TestMail_Content(testEmailCount, Jenkins.getInstance().getDisplayName()), charset);
491
msg.setFrom(StringToAddress(adminAddress, charset));
492
if (StringUtils.isNotBlank(replyToAddress)) {
493
msg.setReplyTo(new Address[]{StringToAddress(replyToAddress, charset)});
495
msg.setSentDate(new Date());
496
msg.setRecipient(Message.RecipientType.TO, StringToAddress(sendTestMailTo, charset));
499
return FormValidation.ok(Messages.Mailer_EmailSentSuccessfully());
500
} catch (MessagingException e) {
501
return FormValidation.errorWithMarkup("<p>"+Messages.Mailer_FailedToSendEmail()+"</p><pre>"+Util.escape(Functions.printThrowable(e))+"</pre>");
505
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
511
* Per user property that is e-mail address.
513
public static class UserProperty extends hudson.model.UserProperty {
515
* The user's e-mail address.
516
* Null to leave it to default.
518
private final String emailAddress;
520
public UserProperty(String emailAddress) {
521
this.emailAddress = emailAddress;
525
public String getAddress() {
526
if(hasExplicitlyConfiguredAddress())
529
// try the inference logic
530
return MailAddressResolver.resolve(user);
534
* Has the user configured a value explicitly (true), or is it inferred (false)?
536
public boolean hasExplicitlyConfiguredAddress() {
537
return Util.fixEmptyAndTrim(emailAddress)!=null;
541
public static final class DescriptorImpl extends UserPropertyDescriptor {
542
public String getDisplayName() {
543
return Messages.Mailer_UserProperty_DisplayName();
546
public UserProperty newInstance(User user) {
547
return new UserProperty(null);
551
public UserProperty newInstance(StaplerRequest req, JSONObject formData) throws FormException {
552
return new UserProperty(req.getParameter("email.address"));
558
* Debug probe point to be activated by the scripting console.
560
public static boolean debug = false;