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

feat: [CO 621] Allow LE certificates to be generated from Mailbox endpoint #175

Merged
merged 39 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d6f841c
feat: [CO-621] add async execution
aheeva-yuliya Mar 13, 2023
ab153e7
feat: [CO-621] comment tests
aheeva-yuliya Mar 13, 2023
ff5ccbb
feat: [CO-621] add send message
aheeva-yuliya Mar 15, 2023
78100ce
feat: [CO-621] able to notify
aheeva-yuliya Mar 16, 2023
58f2a2c
feat: [CO-621] notify domain recipients using MailSender
aheeva-yuliya Mar 17, 2023
b94bd8e
feat: [CO-621] add convert method
aheeva-yuliya Mar 17, 2023
30dd98d
feat: [CO-621] setEnvelopeFrom
aheeva-yuliya Mar 18, 2023
9db3966
feat: [CO-621] added failure messages
aheeva-yuliya Mar 18, 2023
d619095
feat: [CO-621] added / to certbot working dir
aheeva-yuliya Mar 18, 2023
0fd4c90
feat: [CO-621] fix javadoc
aheeva-yuliya Mar 18, 2023
e7933e3
feat: [CO-621] add CertificateNotificationManager and tests
aheeva-yuliya Mar 27, 2023
01cee06
feat: [CO-621] modify CertificateNotificationManager tests
aheeva-yuliya Mar 28, 2023
e752803
feat: [CO-621] tests
aheeva-yuliya Mar 28, 2023
3af10e7
feat: [CO-621] tests
aheeva-yuliya Mar 28, 2023
bbae7b7
feat: [CO-621] tests
aheeva-yuliya Mar 28, 2023
5e6c4fb
feat: [CO-621] uncomment tests
aheeva-yuliya Mar 28, 2023
8bdad8f
feat: [CO-621] comment tests
aheeva-yuliya Mar 28, 2023
aa2bc46
feat: [CO-621] contact and FileIntoCopy tests keep failing
aheeva-yuliya Mar 28, 2023
9d4473f
feat: [CO-621] apply small changes, modify IssueCertTest
aheeva-yuliya Mar 29, 2023
7e044e2
feat: [CO-621] modify java docs
aheeva-yuliya Mar 29, 2023
bbd9fba
feat: [CO-621] add checkValidity method
aheeva-yuliya Mar 30, 2023
6825292
feat: [CO-621] make provisioning to be a field
aheeva-yuliya Mar 30, 2023
1e9f15e
feat: [CO-621] uncomment Contact and FileIntoCopy tests
aheeva-yuliya Mar 30, 2023
e7c8229
feat: [CO-621] slightly modify Contact and FileIntoCopy tests
aheeva-yuliya Mar 30, 2023
1ada9cb
feat: [CO-621] slightly modify ImapHandlerTest
aheeva-yuliya Mar 30, 2023
6c30810
feat: [CO-621] remove checkValidity
aheeva-yuliya Mar 30, 2023
6ea1e95
feat: [CO-621] modify templates
aheeva-yuliya Mar 30, 2023
d2183f3
feat: [CO-621] add domain name to success template
aheeva-yuliya Mar 31, 2023
8474927
feat: [CO-621] refactor to avoid casting
aheeva-yuliya Apr 3, 2023
632322c
feat: [CO-621] delete domainName from fields
aheeva-yuliya Apr 3, 2023
51c7375
feat: [CO-621] apply requested changes
aheeva-yuliya Apr 3, 2023
0c5033b
feat: [CO-621] apply requested changes
aheeva-yuliya Apr 3, 2023
7d3a12e
feat: [CO-621] apply requested changes
aheeva-yuliya Apr 3, 2023
c9558a0
feat: [CO-621] apply requested changes
aheeva-yuliya Apr 3, 2023
c642e08
feat: [CO-621] add small changes to tests
aheeva-yuliya Apr 3, 2023
3c7313f
feat: [CO-621] refactor CertificateNotificationManager, modify tests
aheeva-yuliya Apr 3, 2023
160d086
feat: [CO-621] refactor CertificateNotificationManager, modify tests
aheeva-yuliya Apr 4, 2023
17adeb3
feat: [CO-621] change javadoc
aheeva-yuliya Apr 4, 2023
dd98b69
feat: [CO-621] add logger to tests
aheeva-yuliya Apr 4, 2023
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
53 changes: 52 additions & 1 deletion store/src/main/java/com/zimbra/cs/mailbox/MailSender.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package com.zimbra.cs.mailbox;

import com.zimbra.cs.mailclient.smtp.SmtpTransport;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -25,9 +26,11 @@

import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.URLName;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

Expand Down Expand Up @@ -246,6 +249,27 @@ public MailSender setSession(Account account) throws ServiceException {
return this;
}

/**
* Sets an alternate JavaMail <tt>Session</tt> and SMTP hosts
* that will be used to send the message based on the domain.
* The default behavior is to use SMTP settings from
* the <tt>Session<tt> on the {@link MimeMessage}.
* @throws ServiceException if not able to get SMTP session for the domain
*
* @author Yuliya Aheeva
* @since 23.4.0
frisonisland marked this conversation as resolved.
Show resolved Hide resolved
*/
public MailSender setSession(Domain domain) throws ServiceException {
try {
mSession = JMSession.getSmtpSession(domain);
} catch (MessagingException e) {
throw ServiceException.FAILURE("Unable to get SMTP session for " + domain, e);
}
mSmtpHosts.clear();
mSmtpHosts.addAll(JMSession.getSmtpHosts(domain));
return this;
}
frisonisland marked this conversation as resolved.
Show resolved Hide resolved

/**
* Sets JavaMail <tt>Session</tt> using the SMTP settings associated with the data source.
* @throws ServiceException
Expand Down Expand Up @@ -282,6 +306,16 @@ public MailSender setEnvelopeFrom(String address) {
return this;
}

/**
* Returns the current session.
* @return mSession
* @author Yuliya Aheeva
* @since 23.4.0
*/
public Session getCurrentSession() {
return this.mSession;
}

public static int getSentFolderId(Mailbox mbox, Identity identity) throws ServiceException {
int folderId = Mailbox.ID_FOLDER_SENT;
String sentFolder = identity.getAttr(Provisioning.A_zimbraPrefSentMailFolder, null);
Expand Down Expand Up @@ -1231,7 +1265,9 @@ private void sendMessageToHost(String hostname, MimeMessage mm, Address[] rcptAd
}
ZimbraLog.smtp.debug("Sending message %s to SMTP host %s with properties: %s",
mm.getMessageID(), hostname, mSession.getProperties());
Transport transport = mSession.getTransport("smtp");

Transport transport = getTransport();

try {
transport.connect();
transport.sendMessage(mm, rcptAddresses);
Expand All @@ -1240,6 +1276,21 @@ private void sendMessageToHost(String hostname, MimeMessage mm, Address[] rcptAd
}
}

/**
* Initialize a new SMTP transport if not able to get one from the mSession.
* @return SMTP transport
* @author Yuliya Aheeva
* @since 23.4.0
*/
private Transport getTransport() {
try {
return mSession.getTransport("smtp");
} catch (NoSuchProviderException e) {
URLName urlName = new URLName("smtp", null, -1, null, null, null);
return new SmtpTransport(mSession, urlName);
}
}

private void checkMTAConnectionToHost(String hostname) throws MessagingException {
mSession.getProperties().setProperty("mail.smtp.host", hostname);
if (mEnvelopeFrom != null) {
Expand Down
18 changes: 18 additions & 0 deletions store/src/main/java/com/zimbra/cs/mailbox/Mailbox.java
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,24 @@ public MailSender getMailSender() throws ServiceException {
return sender;
}

/**
* Returns a {@link MailSender} object based on specific domain properties
* that can be used to send mails.
*
* @param domain a domain to get needed properties
* @return {@link MailSender} object
* @throws ServiceException if unable to get SMTP session for the current domain
* @author Yuliya Aheeva
* @since 23.4.0
*/
public MailSender getMailSender(Domain domain) throws ServiceException {
MailSender sender = new MailSender();
sender.setTrackBadHosts(true);
sender.setSession(domain);

return sender;
}

/**
* Returns the list of all <code>Mailbox</code> listeners of a given type. Returns all listeners
* when the passed-in type is <tt>null</tt>.
Expand Down
111 changes: 102 additions & 9 deletions store/src/main/java/com/zimbra/cs/rmgmt/RemoteCertbot.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
package com.zimbra.cs.rmgmt;

import com.zimbra.common.account.Key.AccountBy;
import com.zimbra.common.service.ServiceException;
import com.zimbra.common.util.ZimbraLog;
import com.zimbra.cs.account.Account;
import com.zimbra.cs.account.Domain;
import com.zimbra.cs.account.Provisioning;
import com.zimbra.cs.mailbox.MailSender;
import com.zimbra.cs.mailbox.Mailbox;
import com.zimbra.cs.mailbox.OperationContext;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import javax.mail.Address;
import javax.mail.Message.RecipientType;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

/**
* RemoteCertbot class interacts with "Certbot" - an acme client for managing Let’s Encrypt
Expand Down Expand Up @@ -36,21 +51,27 @@ public RemoteCertbot(RemoteManager remoteManager) {

/**
* Creates a command to be executed by the Certbot acme client.
* E.g. certbot certonly --agree-tos --email admin@test.com -n --webroot -w /opt/zextras
*
* <p>E.g. certbot certonly --agree-tos --email admin@test.com -n --webroot -w /opt/zextras
* --cert-name demo.zextras.io -d acme.demo.zextras.io -d webmail-acme.demo.zextras.io
*
* @param remoteCommand {@link com.zimbra.cs.rmgmt.RemoteCommands}
* @param email domain admin email who tries to execute a command (should be agreed to the
* ACME server's Subscriber Agreement)
* @param email domain admin email who tries to execute a command (should be agreed to the ACME
* server's Subscriber Agreement)
* @param chain long (default) or short (should be specified by domain admin in {@link
* com.zimbra.soap.admin.message.IssueCertRequest} request with the key word "short")
* com.zimbra.soap.admin.message.IssueCertRequest} request with the key word "short")
* @param domainName a value of domain attribute zimbraDomainName
* @param publicServiceHostName a value of domain attribute zimbraPublicServiceHostname
* @param virtualHosts a value/ values of domain attribute zimbraVirtualHostname
* @return created command
*/
public String createCommand(String remoteCommand, String email, String chain,
String domainName, String publicServiceHostName, String[] virtualHosts) {
public String createCommand(
String remoteCommand,
String email,
String chain,
String domainName,
String publicServiceHostName,
String[] virtualHosts) {

this.stringBuilder = new StringBuilder();

Expand All @@ -69,13 +90,27 @@ public String createCommand(String remoteCommand, String email, String chain,
return stringBuilder.toString();
}

/**
* Executes a command asynchronously and notifies domain recipients about the command execution.
*
* @param domain domain
* @param command a command to be executed
* @author Yuliya Aheeva
* @since 23.4.0
*/
public void supplyAsync(Mailbox mbox, Domain domain, String command) {
CompletableFuture.supplyAsync(() -> execute(command))
.thenAccept(message -> notify(mbox, domain, message));
}

/**
* Executes a command using {@link com.zimbra.cs.rmgmt.RemoteManager}.
*
* @param command a command to be executed
* @return a sting message of successful remote execution or detailed exception message
* in case of failure.
* @return a sting message of successful remote execution or detailed exception message in case of
* failure.
*/
public String execute(String command) {
private String execute(String command) {
try {
RemoteResult remoteResult = remoteManager.execute(command);
return new String(remoteResult.getMStdout(), StandardCharsets.UTF_8);
Expand All @@ -84,9 +119,67 @@ public String execute(String command) {
}
}

/**
* Notifies domain recipients about certificate generation result.
*
* @param mbox object of {@link com.zimbra.cs.mailbox.Mailbox} needed for {@link
* com.zimbra.cs.mailbox.MailSender} in order to send message
* @param domain object of {@link com.zimbra.cs.account.Domain} needed to get {@link
* com.zimbra.common.account.ZAttrProvisioning} attributes A_carbonioNotificationRecipients
* and A_carbonioNotificationFrom
* @param message a message returned by Certbot acme client
* @author Yuliya Aheeva
* @since 23.4.0
*/
private void notify(Mailbox mbox, Domain domain, String message) {
String domainName = domain.getName();

ZimbraLog.rmgmt.info("Issuing LetsEncrypt cert command for domain " + domainName
+ " was finished with the following result: " + message);

try {
String from = Optional.ofNullable(domain.getCarbonioNotificationFrom()).orElseThrow(
() -> ServiceException.FAILURE("CarbonioNotificationFrom attribute for the domain "
+ domainName + " is not present.", null));
String[] to = Optional.ofNullable(domain.getCarbonioNotificationRecipients()).orElseThrow(
() -> ServiceException.FAILURE("CarbonioNotificationRecipients attribute for the domain "
+ domainName + " is not present.", null));

String subject = "Let's Encrypt Certificate generation result for " + domainName;

Provisioning prov = Provisioning.getInstance();
Account account = prov.get(AccountBy.name, from);
OperationContext operationContext = new OperationContext(account);

MailSender sender = mbox.getMailSender(domain);
sender.setEnvelopeFrom(from);

MimeMessage mm = new MimeMessage(sender.getCurrentSession());
mm.addFrom(convert(from));
mm.addRecipients(RecipientType.TO, convert(to));
mm.setSubject(subject);
mm.setText(message);
mm.saveChanges();

sender.sendMimeMessage(operationContext, mbox, mm);

ZimbraLog.rmgmt.info("Notifications about LetsEncrypt certificate generation were sent "
+ "for the " + domainName + " recipients.");

} catch (Exception e) {
ZimbraLog.rmgmt.info("Notifications about LetsEncrypt certificate generation weren't sent "
+ "for the " + domainName + " recipients. Sending failure: " + e.getMessage());
}
}

private void addSubCommand(String delimiter, String... params) {
for (String param : params) {
this.stringBuilder.append(delimiter).append(param);
}
}

private Address[] convert(String... addresses) throws AddressException {
String addressList = String.join(", ", addresses);
return InternetAddress.parse(addressList);
}
}
14 changes: 9 additions & 5 deletions store/src/main/java/com/zimbra/cs/service/admin/IssueCert.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.zimbra.cs.account.Provisioning;
import com.zimbra.cs.account.Server;
import com.zimbra.cs.account.accesscontrol.generated.AdminRights;
import com.zimbra.cs.mailbox.Mailbox;
import com.zimbra.cs.rmgmt.RemoteCertbot;
import com.zimbra.cs.rmgmt.RemoteCommands;
import com.zimbra.cs.rmgmt.RemoteManager;
Expand Down Expand Up @@ -95,19 +96,22 @@ public Element handle(final Element request, final Map<String, Object> context)
domainName,
publicServiceHostname,
virtualHostNames);
String result = certbot.execute(command);

ZimbraLog.rmgmt.info(
"Issuing LetsEncrypt cert command for domain " + domainName
+ " was finished with the following result: " + result);
Mailbox mbox = getRequestedMailbox(zsc);

certbot.supplyAsync(mbox, domain, command);

Element response = zsc.createElement(AdminConstants.ISSUE_CERT_RESPONSE);

Element responseMessageElement =
response
.addNonUniqueElement(AdminConstants.E_MESSAGE)
.addAttribute(AdminConstants.A_DOMAIN, domainName);
responseMessageElement.setText(result);

responseMessageElement.setText(
"Your request for the certificate generation has been "
+ "taken and will be processed. Notification recipients would be notified."
);

return response;
}
Expand Down
1 change: 0 additions & 1 deletion store/src/main/java/com/zimbra/cs/session/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import com.zimbra.common.service.ServiceException;
import com.zimbra.common.soap.Element;
import com.zimbra.cs.account.Account;
import com.zimbra.cs.account.AuthToken;
import com.zimbra.cs.mailbox.Mailbox;
import com.zimbra.cs.mailbox.MailboxManager;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public class ProxyConfGen {
private static Provisioning mProv = null;
private static boolean mGenConfPerVhn = false;
private static boolean hasCustomTemplateLocationArg = false;
private static final String CERTBOT_WORKING_DIR = "/common/etc/letsencrypt/live/";
private static final String CERTBOT_WORKING_DIR = "/common/certbot/etc/letsencrypt/live/";
private static final String CERT = "/fullchain.pem";
private static final String KEY = "/privkey.pem";

Expand Down Expand Up @@ -2482,7 +2482,7 @@ private static void updateCertificateKeyPair(final DomainAttrItem entry)
attrs.put(Provisioning.A_zimbraSSLCertificate, certificate);
attrs.put(Provisioning.A_zimbraSSLPrivateKey, privateKey);

LOG.info("Saving " + domainName + " Let's Encrypt certificate/key pair to LDAP.");
LOG.info("Saving " + domainName + " Let's Encrypt certificate/key pair to LDAP");
mProv.modifyAttrs(domain, attrs, true);

entry.sslCertificate = certificate;
Expand Down
Loading