Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-37655] Use Credentials API for SMTP authentication #88

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I need to do anything extra for credentials tracking here, or does a call to lookupCredentials append to the tracking log automatically?

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