Skip to content

Commit

Permalink
JENKINS-37655: Use Credentials API for SMTP authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskilding committed Jan 17, 2022
1 parent d726a11 commit 48c9f9a
Show file tree
Hide file tree
Showing 20 changed files with 473 additions and 94 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ target/
*.iws
*.ipr
.idea
out
out
.DS_Store
3 changes: 1 addition & 2 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ For example, if this field is set to `@acme.org`, then user foo will by default

There are some advanced options as well:

* **Use SMTP Authentication**: check this option to use SMTP authentication when sending out e-mails.
If your environment requires the use of SMTP authentication, specify the user name and the password in the fields shown when this option is checked.
* **Use SMTP Authentication**: this lets you specify a Jenkins Username/Password https://www.jenkins.io/doc/book/using/using-credentials/[credential] to use for SMTP authentication when sending e-mails.
* **Use SSL**: Whether or not to use SSL for connecting to the SMTP server.
Defaults to port `465`.
Other advanced configurations can be done by setting system properties. See this document for possible values and effects.
Expand Down
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>display-url-api</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>junit</artifactId>
Expand Down
94 changes: 28 additions & 66 deletions src/main/java/hudson/tasks/Mailer.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
*/
package hudson.tasks;

import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
Expand All @@ -38,6 +41,7 @@
import hudson.RestrictedSince;
import hudson.Util;
import hudson.model.*;
import hudson.security.ACL;
import jenkins.plugins.mailer.tasks.i18n.Messages;
import hudson.security.Permission;
import hudson.util.FormValidation;
Expand All @@ -56,7 +60,9 @@
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Date;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
Expand Down Expand Up @@ -214,7 +220,7 @@ public static InternetAddress StringToAddress(String strAddress, String charset)
* @throws UnsupportedEncodingException Unsupported encoding
* @since TODO
*/
public static @NonNull InternetAddress stringToAddress(@NonNull String strAddress,
public static @NonNull InternetAddress stringToAddress(@NonNull String strAddress,
@NonNull String charset) throws AddressException, UnsupportedEncodingException {
Matcher m = ADDRESS_PATTERN.matcher(strAddress);
if(!m.matches()) {
Expand Down Expand Up @@ -262,7 +268,6 @@ public static final class DescriptorImpl extends BuildStepDescriptor<Publisher>
@Deprecated
private transient String smtpAuthUsername;


/** @deprecated as of 1.23, use {@link #authentication} */
@Deprecated
private transient Secret smtpAuthPassword;
Expand Down Expand Up @@ -364,16 +369,17 @@ public void setReplyToAddress(String address) {
* @return mail session based on the underlying session parameters.
*/
public Session createSession() {
return createSession(smtpHost,smtpPort,useSsl,useTls,getSmtpAuthUserName(),getSmtpAuthPasswordSecret());
String credentialsId = Optional.ofNullable(authentication).map(auth -> auth.getCredentialsId()).orElse(null);
return createSession(smtpHost,smtpPort,useSsl,useTls,credentialsId);
}
private static Session createSession(String smtpHost, String smtpPort, boolean useSsl, boolean useTls, String smtpAuthUserName, Secret smtpAuthPassword) {

private static Session createSession(String smtpHost, String smtpPort, boolean useSsl, boolean useTls, String credentialsId) {
final String SMTP_PORT_PROPERTY = "mail.smtp.port";
final String SMTP_SOCKETFACTORY_PORT_PROPERTY = "mail.smtp.socketFactory.port";
final String SMTP_SSL_ENABLE_PROPERTY = "mail.smtp.ssl.enable";

smtpHost = Util.fixEmptyAndTrim(smtpHost);
smtpPort = Util.fixEmptyAndTrim(smtpPort);
smtpAuthUserName = Util.fixEmptyAndTrim(smtpAuthUserName);

Properties props = new Properties(System.getProperties());
if(smtpHost!=null) {
Expand Down Expand Up @@ -419,26 +425,22 @@ private static Session createSession(String smtpHost, String smtpPort, boolean u
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.starttls.required", "true");
}
if(smtpAuthUserName!=null)
props.put("mail.smtp.auth","true");

Optional<Authenticator> authenticator = Optional.ofNullable(credentialsId)
.map(id -> CredentialsMatchers.firstOrNull(
CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials.class, (Item) null, ACL.SYSTEM, Collections.emptyList()),
CredentialsMatchers.withId(id)))
.map(creds -> new StandardUsernamePasswordCredentialsAuthenticator(creds));
if (authenticator.isPresent()) {
LOGGER.fine(String.format("Sending mail with SMTP authentication (credential ID: %s)", credentialsId));
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);
}
};
return Session.getInstance(props, authenticator.orElse(null));
}

@Override
Expand All @@ -449,7 +451,7 @@ public boolean configure(StaplerRequest req, JSONObject json) throws FormExcepti
// case of failure to databind, it gets reverted to previous value.
// Would not be necessary by https://github.com/jenkinsci/jenkins/pull/3669
SMTPAuthentication current = this.authentication;

try (BulkChange b = new BulkChange(this)) {
this.authentication = null;
req.bindJSON(this, json);
Expand All @@ -471,7 +473,7 @@ public String getSmtpHost() {
return smtpHost;
}


/** @deprecated as of 1.23, use {@link #getSmtpHost()} */
@Deprecated
public String getSmtpServer() {
Expand Down Expand Up @@ -513,31 +515,6 @@ public String getUrl() {
return getJenkinsLocationConfiguration().getUrl();
}

/**
* @deprecated as of 1.21
* Use {@link #authentication}
*/
@Deprecated
public String getSmtpAuthUserName() {
if (authentication == null) return null;
return authentication.getUsername();
}

/**
* @deprecated as of 1.21
* Use {@link #authentication}
*/
@Deprecated
public String getSmtpAuthPassword() {
if (authentication == null) return null;
return Secret.toString(authentication.getPassword());
}

public Secret getSmtpAuthPasswordSecret() {
if (authentication == null) return null;
return authentication.getPassword();
}

public boolean getUseSsl() {
return useSsl;
}
Expand Down Expand Up @@ -626,19 +603,6 @@ public SMTPAuthentication getAuthentication() {
return authentication;
}

/**
* @deprecated as of 1.21
* Use {@link #authentication}
*/
@Deprecated
public void setSmtpAuth(String userName, String password) {
if (userName == null && password == null) {
this.authentication = null;
} else {
this.authentication = new SMTPAuthentication(userName, Secret.fromString(password));
}
}

@Override
public Publisher newInstance(StaplerRequest req, JSONObject formData) throws FormException {
Mailer m = (Mailer)super.newInstance(req, formData);
Expand Down Expand Up @@ -693,8 +657,7 @@ public FormValidation doCheckDefaultSuffix(@QueryParameter String value) {
* @param smtpHost name of the SMTP server to use for mail sending
* @param adminAddress Jenkins administrator mail address
* @param authentication if set to {@code true} SMTP is used without authentication (username and password)
* @param username plaintext username for SMTP authentication
* @param password secret password for SMTP authentication
* @param credentialsId Jenkins Username/Password credential for SMTP authentication
* @param useSsl if set to {@code true} SSL is used
* @param useTls if set to {@code true} TLS is used
* @param smtpPort port to use for SMTP transfer
Expand All @@ -705,17 +668,16 @@ public FormValidation doCheckDefaultSuffix(@QueryParameter String value) {
@RequirePOST
public FormValidation doSendTestMail(
@QueryParameter String smtpHost, @QueryParameter String adminAddress, @QueryParameter boolean authentication,
@QueryParameter String username, @QueryParameter Secret password,
@QueryParameter String credentialsId,
@QueryParameter boolean useSsl, @QueryParameter boolean useTls, @QueryParameter String smtpPort, @QueryParameter String charset,
@QueryParameter String sendTestMailTo) throws IOException {
try {
Jenkins.get().checkPermission(DescriptorImpl.getJenkinsManageOrAdmin());
if (!authentication) {
username = null;
password = null;
credentialsId = null;
}

MimeMessage msg = new MimeMessage(createSession(smtpHost, smtpPort, useSsl, useTls, username, password));
MimeMessage msg = new MimeMessage(createSession(smtpHost, smtpPort, useSsl, useTls, credentialsId));
msg.setSubject(Messages.Mailer_TestMail_Subject(testEmailCount.incrementAndGet()), charset);
msg.setText(Messages.Mailer_TestMail_Content(testEmailCount.get(), Jenkins.get().getDisplayName()), charset);
msg.setFrom(stringToAddress(adminAddress, charset));
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/hudson/tasks/Retryable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package hudson.tasks;

class Retryable {

private Retryable() {

}

interface Fn {
void run(int attempt) throws Exception;
}

/**
* Run the provided function once, and if it fails, retry up to n times.
*
* This does not support asynchronous functions.
*
* @param times the number of times to retry after a failure. When times=0, the function runs once, with no retries if it fails. When times=2, the function runs once, with up to 2 retries if it fails.
* @param fn the function to run and retry
* @return whether any of the function invocations succeeded
*/
static boolean retry(int times, Fn fn) {
int attempt = 0;

do {
try {
fn.run(attempt + 1);

return true;
} catch (Exception e) {
// try again
}

++attempt;
} while (attempt <= times);

return false;
}
}
Loading

0 comments on commit 48c9f9a

Please sign in to comment.