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