From 6c8daeab9c1b6460785582f904bf693b7b635df3 Mon Sep 17 00:00:00 2001 From: shubhamCS03 Date: Thu, 23 Mar 2023 13:03:42 +0530 Subject: [PATCH] ZCS-13176:Mail recall zimlet message verfication header --- .../common/account/ZAttrProvisioning.java | 8 ++ .../common/service/ServiceException.java | 5 + .../zimbra/common/soap/AdminConstants.java | 6 ++ .../com/zimbra/common/soap/MailConstants.java | 7 ++ soap/src/java/com/zimbra/soap/JaxbUtil.java | 10 +- .../message/GenerateSecretKeyRequest.java | 39 ++++++++ .../message/GenerateSecretKeyResponse.java | 34 +++++++ .../soap/mail/message/MailRecallRequest.java | 39 ++++++++ .../soap/mail/message/MailRecallResponse.java | 64 ++++++++++++ store/conf/attrs/zimbra-attrs.xml | 6 +- .../com/zimbra/cs/account/ZAttrConfig.java | 72 ++++++++++++++ .../callback/GenerateSecretKeyCallback.java | 68 +++++++++++++ .../com/zimbra/cs/mailbox/MailSender.java | 20 +++- .../zimbra/cs/service/admin/AdminService.java | 3 + .../cs/service/admin/GenerateSecretKey.java | 56 +++++++++++ .../com/zimbra/cs/service/util/SecretKey.java | 97 +++++++++++++++++++ 16 files changed, 530 insertions(+), 4 deletions(-) create mode 100644 soap/src/java/com/zimbra/soap/admin/message/GenerateSecretKeyRequest.java create mode 100644 soap/src/java/com/zimbra/soap/admin/message/GenerateSecretKeyResponse.java create mode 100644 soap/src/java/com/zimbra/soap/mail/message/MailRecallRequest.java create mode 100644 soap/src/java/com/zimbra/soap/mail/message/MailRecallResponse.java create mode 100644 store/src/java/com/zimbra/cs/account/callback/GenerateSecretKeyCallback.java create mode 100644 store/src/java/com/zimbra/cs/service/admin/GenerateSecretKey.java create mode 100644 store/src/java/com/zimbra/cs/service/util/SecretKey.java diff --git a/common/src/java/com/zimbra/common/account/ZAttrProvisioning.java b/common/src/java/com/zimbra/common/account/ZAttrProvisioning.java index 40544f66f94..70f0ff6dcc1 100644 --- a/common/src/java/com/zimbra/common/account/ZAttrProvisioning.java +++ b/common/src/java/com/zimbra/common/account/ZAttrProvisioning.java @@ -7221,6 +7221,14 @@ public static TwoFactorAuthSecretEncoding fromString(String s) throws ServiceExc @ZAttr(id=4094) public static final String A_zimbraFeatureMailRecallEnabled = "zimbraFeatureMailRecallEnabled"; + /** + * Secret key used in Mail Recall to make it more secure from spoof. + * + * @since ZCS 10.1.5 + */ + @ZAttr(id=4136) + public static final String A_zimbraFeatureMailRecallSecretKey = "zimbraFeatureMailRecallSecretKey"; + /** * Time(in minutes) within which a message can be recalled. The default * time is 30 minutes and accepts value from 1 to 30. diff --git a/common/src/java/com/zimbra/common/service/ServiceException.java b/common/src/java/com/zimbra/common/service/ServiceException.java index b86693256eb..d3ddca8991d 100644 --- a/common/src/java/com/zimbra/common/service/ServiceException.java +++ b/common/src/java/com/zimbra/common/service/ServiceException.java @@ -60,6 +60,7 @@ public class ServiceException extends Exception { public static final String UNSUPPORTED = "service.UNSUPPORTED"; public static final String FORBIDDEN = "service.FORBIDDEN"; public static final String SIEVE_SCRIPT_MAX_SIZE_EXCEPTION = "service.SIEVE_SCRIPT_MAX_SIZE_EXCEPTION"; + public static final String MAIL_RECALL_ERROR = "service.MAIL_RECALL_ERROR"; // generic "not found" error for objects other than mail items public static final String NOT_FOUND = "service.NOT_FOUND"; @@ -303,6 +304,10 @@ public static ServiceException LICENSE_ERROR(String message, Throwable cause) { return new ServiceException("license error: "+message, LICENSE_ERROR, RECEIVERS_FAULT, cause); } + public static ServiceException ERROR_MESSAGE(String str, Throwable cause){ + return new ServiceException(String.format("mailRecall error: %s", str), MAIL_RECALL_ERROR, SENDERS_FAULT, cause); + } + public static ServiceException ERROR_WHILE_PARSING_UPLOAD(String message, Throwable cause) { return new ServiceException( String.format("ioexception during upload: %s", message), ERROR_WHILE_PARSING_UPLOAD, RECEIVERS_FAULT, cause); diff --git a/common/src/java/com/zimbra/common/soap/AdminConstants.java b/common/src/java/com/zimbra/common/soap/AdminConstants.java index 5b72e6cd7a5..2c763292d30 100644 --- a/common/src/java/com/zimbra/common/soap/AdminConstants.java +++ b/common/src/java/com/zimbra/common/soap/AdminConstants.java @@ -1647,6 +1647,12 @@ public final class AdminConstants { public static final QName VALIDATE_S3_BUCKET_REACHABLE_REQUEST = QName.get(E_VALIDATE_S3_BUCKET_REACHABLE_REQUEST, NAMESPACE); public static final QName VALIDATE_S3_BUCKET_REACHABLE_RESPONSE = QName.get(E_VALIDATE_S3_BUCKET_REACHABLE_RESPONSE, NAMESPACE); + // Secret key for mail recall + public static final String E_GENERATE_SECRET_KEY_REQUEST = "GenerateSecretKeyRequest"; + public static final String E_GENERATE_SECRET_KEY_RESPONSE = "GenerateSecretKeyResponse"; + public static final QName GENERATE_SECRET_KEY_REQUEST = QName.get(E_GENERATE_SECRET_KEY_REQUEST, NAMESPACE); + public static final QName GENERATE_SECRET_KEY_RESPONSE = QName.get(E_GENERATE_SECRET_KEY_RESPONSE, NAMESPACE); + // Removed Zetras zimlet package list public static final List ZEXTRAS_PACKAGES_LIST = Arrays.asList("com_ng_auth", "com_zextras_zextras", "com_zextras_client", "com_zimbra_connect_classic", "com_zimbra_connect_modern", "com_zextras_docs", diff --git a/common/src/java/com/zimbra/common/soap/MailConstants.java b/common/src/java/com/zimbra/common/soap/MailConstants.java index 69b00a11936..9a9d6f5e58b 100644 --- a/common/src/java/com/zimbra/common/soap/MailConstants.java +++ b/common/src/java/com/zimbra/common/soap/MailConstants.java @@ -1425,4 +1425,11 @@ private MailConstants() { public static final QName RECOVER_ACCOUNT_RESPONSE = QName.get(E_RECOVER_ACCOUNT_RESPONSE, NAMESPACE); public static final String A_RECOVERY_ACCOUNT = "recoveryAccount"; public static final String A_CHANNEL = "channel"; + + // MailRecall Uses + public static final String E_MAIL_RECALL_REQUEST = "MailRecallRequest"; + public static final String E_MAIL_RECALL_RESPONSE = "MailRecallResponse"; + public static final QName MAIL_RECALL_REQUEST = QName.get(E_MAIL_RECALL_REQUEST, NAMESPACE); + public static final QName MAIL_RECALL_RESPONSE = QName.get(E_MAIL_RECALL_RESPONSE, NAMESPACE); + public static final String ITEM_ID = "itemId"; } diff --git a/soap/src/java/com/zimbra/soap/JaxbUtil.java b/soap/src/java/com/zimbra/soap/JaxbUtil.java index 2ba30172be2..ab1081733b2 100644 --- a/soap/src/java/com/zimbra/soap/JaxbUtil.java +++ b/soap/src/java/com/zimbra/soap/JaxbUtil.java @@ -17,6 +17,8 @@ package com.zimbra.soap; +import com.zimbra.soap.mail.message.MailRecallRequest; +import com.zimbra.soap.mail.message.MailRecallResponse; import java.io.ByteArrayInputStream; import java.util.HashMap; import java.util.Iterator; @@ -1086,6 +1088,8 @@ public final class JaxbUtil { com.zimbra.soap.mail.message.VerifyCodeResponse.class, com.zimbra.soap.mail.message.WaitSetRequest.class, com.zimbra.soap.mail.message.WaitSetResponse.class, + com.zimbra.soap.mail.message.MailRecallRequest.class, + com.zimbra.soap.mail.message.MailRecallResponse.class, com.zimbra.soap.replication.message.BecomeMasterRequest.class, com.zimbra.soap.replication.message.BecomeMasterResponse.class, com.zimbra.soap.replication.message.BringDownServiceIPRequest.class, @@ -1173,7 +1177,9 @@ public final class JaxbUtil { com.zimbra.soap.admin.message.ValidateS3BucketReachableRequest.class, com.zimbra.soap.admin.message.ValidateS3BucketReachableResponse.class, com.zimbra.soap.admin.message.EditS3BucketConfigRequest.class, - com.zimbra.soap.admin.message.EditS3BucketConfigResponse.class + com.zimbra.soap.admin.message.EditS3BucketConfigResponse.class, + com.zimbra.soap.admin.message.GenerateSecretKeyRequest.class, + com.zimbra.soap.admin.message.GenerateSecretKeyResponse.class }; try { @@ -1683,7 +1689,7 @@ public static ModifyItemNotification getModifiedItemSOAP(BaseItemInfo mod, int r ImapMessageInfo messageInfo = new ImapMessageInfo(mod.getIdInMailbox(), mod.getImapUid(), mod.getMailItemType().toString(), mod.getFlagBitmask(), tags); return new ModifyNotification.ModifyItemNotification(messageInfo, reason); } - + public static DeleteItemNotification getDeletedItemSOAP(int itemId, String type) throws ServiceException { return new DeleteItemNotification(itemId, type); } diff --git a/soap/src/java/com/zimbra/soap/admin/message/GenerateSecretKeyRequest.java b/soap/src/java/com/zimbra/soap/admin/message/GenerateSecretKeyRequest.java new file mode 100644 index 00000000000..346b3f146c4 --- /dev/null +++ b/soap/src/java/com/zimbra/soap/admin/message/GenerateSecretKeyRequest.java @@ -0,0 +1,39 @@ +/* + * ***** BEGIN LICENSE BLOCK ***** + * Zimbra Collaboration Suite Server + * Copyright (C) 2025 Synacor, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software Foundation, + * version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . + * ***** END LICENSE BLOCK ***** + */ + +package com.zimbra.soap.admin.message; + +import com.zimbra.common.soap.AdminConstants; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +/** + * @zm-api-command-auth-required true + * @zm-api-command-admin-auth-required true + * @zm-api-command-description Create random secret key and update it to LDAP attribute. + */ +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = AdminConstants.E_GENERATE_SECRET_KEY_REQUEST) +@XmlType(propOrder = {}) +public class GenerateSecretKeyRequest { + + public GenerateSecretKeyRequest() { + } +} diff --git a/soap/src/java/com/zimbra/soap/admin/message/GenerateSecretKeyResponse.java b/soap/src/java/com/zimbra/soap/admin/message/GenerateSecretKeyResponse.java new file mode 100644 index 00000000000..08a8e9e7760 --- /dev/null +++ b/soap/src/java/com/zimbra/soap/admin/message/GenerateSecretKeyResponse.java @@ -0,0 +1,34 @@ +/* + * ***** BEGIN LICENSE BLOCK ***** + * Zimbra Collaboration Suite Server + * Copyright (C) 2025 Synacor, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software Foundation, + * version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . + * ***** END LICENSE BLOCK ***** + */ + +package com.zimbra.soap.admin.message; + +import com.zimbra.common.soap.AdminConstants; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = AdminConstants.E_GENERATE_SECRET_KEY_RESPONSE) +@XmlType(propOrder = {}) +public class GenerateSecretKeyResponse { + + public GenerateSecretKeyResponse() { + } +} diff --git a/soap/src/java/com/zimbra/soap/mail/message/MailRecallRequest.java b/soap/src/java/com/zimbra/soap/mail/message/MailRecallRequest.java new file mode 100644 index 00000000000..3ced538e4a1 --- /dev/null +++ b/soap/src/java/com/zimbra/soap/mail/message/MailRecallRequest.java @@ -0,0 +1,39 @@ +/* + * ***** BEGIN LICENSE BLOCK ***** + * Zimbra Collaboration Suite Server + * Copyright (C) 2025 Synacor, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software Foundation, + * version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . + * ***** END LICENSE BLOCK ***** + */ + +package com.zimbra.soap.mail.message; + +import com.zimbra.common.soap.MailConstants; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = MailConstants.E_MAIL_RECALL_REQUEST) +public class MailRecallRequest { + @XmlAttribute(name = MailConstants.ITEM_ID , required = true) + private int itemId; + + public int getItemId() { + return itemId; + } + + public void setItemId(int itemId) { + this.itemId = itemId; + } +} \ No newline at end of file diff --git a/soap/src/java/com/zimbra/soap/mail/message/MailRecallResponse.java b/soap/src/java/com/zimbra/soap/mail/message/MailRecallResponse.java new file mode 100644 index 00000000000..4e32d2ed09c --- /dev/null +++ b/soap/src/java/com/zimbra/soap/mail/message/MailRecallResponse.java @@ -0,0 +1,64 @@ +/* + * ***** BEGIN LICENSE BLOCK ***** + * Zimbra Collaboration Suite Server + * Copyright (C) 2025 Synacor, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software Foundation, + * version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . + * ***** END LICENSE BLOCK ***** + */ + +package com.zimbra.soap.mail.message; + +import com.zimbra.common.soap.MailConstants; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = MailConstants.E_MAIL_RECALL_RESPONSE) +public class MailRecallResponse { + + @XmlElement(name = "successfulRecall", required = true) + private int successfulRecall; + + @XmlElement(name = "unsuccessfulRecall", required = true) + private int unsuccessfulRecall; + + @XmlElement(name = "allMailRecalled", required = true) + private boolean allMailRecalled; + + // Getters and Setters + public int getSuccessfulRecall() { + return successfulRecall; + } + + public void setSuccessfulRecall(int successfulRecall) { + this.successfulRecall = successfulRecall; + } + + public int getUnsuccessfulRecall() { + return unsuccessfulRecall; + } + + public void setUnsuccessfulRecall(int unsuccessfulRecall) { + this.unsuccessfulRecall = unsuccessfulRecall; + } + + public boolean isAllMailRecalled() { + return allMailRecalled; + } + + public void setAllMailRecalled(boolean allMailRecalled) { + this.allMailRecalled = allMailRecalled; + } +} + diff --git a/store/conf/attrs/zimbra-attrs.xml b/store/conf/attrs/zimbra-attrs.xml index 3661de4d336..02edbd001a9 100644 --- a/store/conf/attrs/zimbra-attrs.xml +++ b/store/conf/attrs/zimbra-attrs.xml @@ -10339,7 +10339,7 @@ TODO: delete them permanently from here Can be used by an external service to indicate that a trial is set to convert to active at expiration. - + FALSE FALSE Enables the mail recall functionality @@ -10560,4 +10560,8 @@ TODO: delete them permanently from here FALSE Feature to enable delivery status notification + + + Secret key used in Mail Recall to make it more secure from spoof. + \ No newline at end of file diff --git a/store/src/java/com/zimbra/cs/account/ZAttrConfig.java b/store/src/java/com/zimbra/cs/account/ZAttrConfig.java index 4e541df0fad..e216232ac83 100644 --- a/store/src/java/com/zimbra/cs/account/ZAttrConfig.java +++ b/store/src/java/com/zimbra/cs/account/ZAttrConfig.java @@ -18823,6 +18823,78 @@ public Map unsetFeatureMailRecallEnabled(Map attrs return attrs; } + /** + * Secret key used in Mail Recall to make it more secure from spoof. + * + * @return zimbraFeatureMailRecallSecretKey, or null if unset + * + * @since ZCS 10.1.5 + */ + @ZAttr(id=4136) + public String getFeatureMailRecallSecretKey() { + return getAttr(Provisioning.A_zimbraFeatureMailRecallSecretKey, null, true); + } + + /** + * Secret key used in Mail Recall to make it more secure from spoof. + * + * @param zimbraFeatureMailRecallSecretKey new value + * @throws com.zimbra.common.service.ServiceException if error during update + * + * @since ZCS 10.1.5 + */ + @ZAttr(id=4136) + public void setFeatureMailRecallSecretKey(String zimbraFeatureMailRecallSecretKey) throws com.zimbra.common.service.ServiceException { + HashMap attrs = new HashMap(); + attrs.put(Provisioning.A_zimbraFeatureMailRecallSecretKey, zimbraFeatureMailRecallSecretKey); + getProvisioning().modifyAttrs(this, attrs); + } + + /** + * Secret key used in Mail Recall to make it more secure from spoof. + * + * @param zimbraFeatureMailRecallSecretKey new value + * @param attrs existing map to populate, or null to create a new map + * @return populated map to pass into Provisioning.modifyAttrs + * + * @since ZCS 10.1.5 + */ + @ZAttr(id=4136) + public Map setFeatureMailRecallSecretKey(String zimbraFeatureMailRecallSecretKey, Map attrs) { + if (attrs == null) attrs = new HashMap(); + attrs.put(Provisioning.A_zimbraFeatureMailRecallSecretKey, zimbraFeatureMailRecallSecretKey); + return attrs; + } + + /** + * Secret key used in Mail Recall to make it more secure from spoof. + * + * @throws com.zimbra.common.service.ServiceException if error during update + * + * @since ZCS 10.1.5 + */ + @ZAttr(id=4136) + public void unsetFeatureMailRecallSecretKey() throws com.zimbra.common.service.ServiceException { + HashMap attrs = new HashMap(); + attrs.put(Provisioning.A_zimbraFeatureMailRecallSecretKey, ""); + getProvisioning().modifyAttrs(this, attrs); + } + + /** + * Secret key used in Mail Recall to make it more secure from spoof. + * + * @param attrs existing map to populate, or null to create a new map + * @return populated map to pass into Provisioning.modifyAttrs + * + * @since ZCS 10.1.5 + */ + @ZAttr(id=4136) + public Map unsetFeatureMailRecallSecretKey(Map attrs) { + if (attrs == null) attrs = new HashMap(); + attrs.put(Provisioning.A_zimbraFeatureMailRecallSecretKey, ""); + return attrs; + } + /** * Time(in minutes) within which a message can be recalled. The default * time is 30 minutes and accepts value from 1 to 30. diff --git a/store/src/java/com/zimbra/cs/account/callback/GenerateSecretKeyCallback.java b/store/src/java/com/zimbra/cs/account/callback/GenerateSecretKeyCallback.java new file mode 100644 index 00000000000..a602a38b4e6 --- /dev/null +++ b/store/src/java/com/zimbra/cs/account/callback/GenerateSecretKeyCallback.java @@ -0,0 +1,68 @@ +/* + * ***** BEGIN LICENSE BLOCK ***** + * Zimbra Collaboration Suite Server + * Copyright (C) 2025 Synacor, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software Foundation, + * version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . + * ***** END LICENSE BLOCK ***** + */ + +package com.zimbra.cs.account.callback; + +import com.google.common.base.Strings; +import com.zimbra.common.service.ServiceException; +import com.zimbra.common.util.ZimbraLog; +import com.zimbra.cs.account.AttributeCallback; +import com.zimbra.cs.account.Entry; +import com.zimbra.cs.account.Provisioning; +import com.zimbra.cs.account.soap.SoapProvisioning; +import com.zimbra.soap.admin.message.GenerateSecretKeyRequest; + +import java.util.Map; +import java.util.Optional; + +public class GenerateSecretKeyCallback extends AttributeCallback { + + @Override + public void preModify(CallbackContext context, String attrName, Object value, + Map attrsToModify, Entry entry) throws ServiceException { + try { + Optional.ofNullable(value) + .map(Object::toString) + .map(String::trim) + .filter("TRUE"::equals) + .ifPresent(this::generateMailRecallSecretKey); + } catch (RuntimeException e) { + throw ServiceException.FAILURE("Unable to initialize SecureRandom for mail recall", e); + } + } + + private void generateMailRecallSecretKey(String value) { + try { + String secretKey = Provisioning.getInstance() + .getConfig() + .getFeatureMailRecallSecretKey(); + + if (Strings.isNullOrEmpty(secretKey)) { + SoapProvisioning sp = SoapProvisioning.getAdminInstance(); + sp.invokeJaxb(new GenerateSecretKeyRequest()); + } + } catch (ServiceException e) { + ZimbraLog.misc.error("Error generating mail recall secret key", e); + throw new RuntimeException("Error generating mail recall secret key", e); + } + } + + @Override + public void postModify(CallbackContext context, String attrName, Entry entry) { + } + +} diff --git a/store/src/java/com/zimbra/cs/mailbox/MailSender.java b/store/src/java/com/zimbra/cs/mailbox/MailSender.java index 74e06cfe0d5..cadde6280fc 100644 --- a/store/src/java/com/zimbra/cs/mailbox/MailSender.java +++ b/store/src/java/com/zimbra/cs/mailbox/MailSender.java @@ -33,6 +33,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import javax.mail.Address; @@ -41,6 +42,7 @@ import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; +import javax.mail.internet.MailDateFormat; import javax.mail.internet.MimeMessage; import com.google.common.base.Joiner; @@ -81,6 +83,7 @@ import com.zimbra.cs.service.FileUploadServlet.Upload; import com.zimbra.cs.service.UserServlet; import com.zimbra.cs.service.util.ItemId; +import com.zimbra.cs.service.util.SecretKey; import com.zimbra.cs.util.AccountUtil; import com.zimbra.cs.util.AccountUtil.AccountAddressMatcher; import com.zimbra.cs.util.BuildInfo; @@ -91,6 +94,8 @@ public class MailSender { public static final String MSGTYPE_REPLY = String.valueOf(Flag.toChar(Flag.ID_REPLIED)); public static final String MSGTYPE_FORWARD = String.valueOf(Flag.toChar(Flag.ID_FORWARDED)); private static Map mPreSendMailListeners = new ConcurrentHashMap(); + private static final MailDateFormat mailDateFormat = new MailDateFormat(); + private static final String X_MESSAGE_VERIFICATION = "X-Zimbra-Message-Verification"; private Boolean mSaveToSent; private Collection mUploads; @@ -1020,7 +1025,18 @@ void updateHeaders(MimeMessage mm, Account acct, Account authuser, OperationCont mm.setFrom(from); mm.setSender(sender); - mm.setSentDate(new Date()); + Date date = new Date(); + mm.setSentDate(date); + + Optional.ofNullable(SecretKey.getMessageVerificationHeaderValue(mm.getMessageID(), mailDateFormat.format(date), + from.getAddress())).ifPresent(value -> { + try { + mm.addHeader(X_MESSAGE_VERIFICATION, value); + } catch (MessagingException e) { + throw new RuntimeException(String.format("Failed to add Message Verification header: %s", value), e); + } + }); + if (sender == null) { Address[] existingReplyTos = mm.getReplyTo(); if (existingReplyTos == null || existingReplyTos.length == 0) { @@ -1483,4 +1499,6 @@ public static void unregisterPreSendMailListener(PreSendMailListener listener) { } } } + + } diff --git a/store/src/java/com/zimbra/cs/service/admin/AdminService.java b/store/src/java/com/zimbra/cs/service/admin/AdminService.java index 918f2f7cca9..4c24135cbf7 100644 --- a/store/src/java/com/zimbra/cs/service/admin/AdminService.java +++ b/store/src/java/com/zimbra/cs/service/admin/AdminService.java @@ -312,6 +312,9 @@ public void registerHandlers(DocumentDispatcher dispatcher) { // ContactBackup API dispatcher.registerHandler(AdminConstants.CONTACT_BACKUP_REQUEST, new ContactBackup()); + + // GetSecretKey API + dispatcher.registerHandler(AdminConstants.GENERATE_SECRET_KEY_REQUEST, new GenerateSecretKey()); } /** diff --git a/store/src/java/com/zimbra/cs/service/admin/GenerateSecretKey.java b/store/src/java/com/zimbra/cs/service/admin/GenerateSecretKey.java new file mode 100644 index 00000000000..19f0ef0cc41 --- /dev/null +++ b/store/src/java/com/zimbra/cs/service/admin/GenerateSecretKey.java @@ -0,0 +1,56 @@ +/* + * ***** BEGIN LICENSE BLOCK ***** + * Zimbra Collaboration Suite Server + * Copyright (C) 2025 Synacor, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software Foundation, + * version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . + * ***** END LICENSE BLOCK ***** + */ + +package com.zimbra.cs.service.admin; + +import com.zimbra.common.service.ServiceException; +import com.zimbra.common.soap.AdminConstants; +import com.zimbra.common.soap.Element; +import com.zimbra.common.util.ZimbraLog; +import com.zimbra.cs.account.Provisioning; +import com.zimbra.cs.account.soap.SoapProvisioning; +import com.zimbra.cs.service.util.SecretKey; +import com.zimbra.soap.ZimbraSoapContext; +import com.zimbra.soap.admin.type.CacheEntryType; + +import java.util.Map; + +public class GenerateSecretKey extends AdminDocumentHandler { + public Element handle(Element request, Map context) throws ServiceException { + ZimbraSoapContext zsc = getZimbraSoapContext(context); + + String randomString = SecretKey.generateRandomString(); + Provisioning.getInstance().getConfig().setFeatureMailRecallSecretKey(randomString); + flushCache(); + + Element response = zsc.createElement(AdminConstants.GENERATE_SECRET_KEY_RESPONSE); + return response; + } + + private void flushCache() { + try { + SoapProvisioning soapProvisioning = new SoapProvisioning(); + soapProvisioning.soapSetURI(SoapProvisioning.getLocalConfigURI()); + soapProvisioning.soapZimbraAdminAuthenticate(); + soapProvisioning.flushCache(CacheEntryType.config.toString(), null, true); + } catch (ServiceException e) { + ZimbraLog.misc.warn("Encountered exception during FlushCache after creating Secret Key", e); + } + } + +} + diff --git a/store/src/java/com/zimbra/cs/service/util/SecretKey.java b/store/src/java/com/zimbra/cs/service/util/SecretKey.java new file mode 100644 index 00000000000..c81c7fe1998 --- /dev/null +++ b/store/src/java/com/zimbra/cs/service/util/SecretKey.java @@ -0,0 +1,97 @@ +/* + * ***** BEGIN LICENSE BLOCK ***** + * Zimbra Collaboration Suite Server + * Copyright (C) 2025 Synacor, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software Foundation, + * version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . + * ***** END LICENSE BLOCK ***** + */ + +package com.zimbra.cs.service.util; + +import com.zimbra.common.service.ServiceException; +import com.zimbra.cs.account.Provisioning; +import java.io.IOException; +import org.apache.commons.codec.binary.Hex; + +import javax.mail.MessagingException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; + +public class SecretKey { + + public static final int KEY_SIZE_BYTES = 32; + public static final String MSGVRFY_HEADER_PREFIX = "hash=SHA256;guid="; + public static final String MSGVRFY_ALGORITHM_NAME = "SHA-256"; + public static final String HEX_64_ZERO_PADDED_FORMAT = "%064x"; + + /** + * returns the randomly generated String + * + * @return randomly generated String + * @throws ServiceException if an error occurred + */ + public static String generateRandomString() throws ServiceException { + try { + SecureRandom random = new SecureRandom(); + byte[] key = new byte[KEY_SIZE_BYTES]; + random.nextBytes(key); + return Base64.getEncoder().encodeToString(key); + } catch (IllegalArgumentException e) { + throw ServiceException.FAILURE("Illegal argument exception occurred during Base64 encoding", e); + } catch (SecurityException e) { + throw ServiceException.FAILURE("Security exception occurred while initializing SecureRandom", e); + } + } + + /** + * provide the Hash for Message-Verification header field. + * + * @param id + * @param date + * @param from + * @throws ServiceException + */ + public static String getMessageVerificationHeaderValue(String id, String date, String from) throws MessagingException, ServiceException { + String secretKey = Provisioning.getInstance().getConfig().getFeatureMailRecallSecretKey(); + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(id); + stringBuilder.append(date); + stringBuilder.append(from); + stringBuilder.append(secretKey); + + String guid = stringBuilder.toString(); + String guidHash = getHashForMessageVerification(guid); + return MSGVRFY_HEADER_PREFIX.concat(guidHash); + } + + /** + * Create a digest of the given input with the given algorithm. + * + * @param input + * @throws ServiceException + */ + private static String getHashForMessageVerification(String input) throws ServiceException { + try { + MessageDigest messageDigest = MessageDigest.getInstance(MSGVRFY_ALGORITHM_NAME); + byte[] hashBytes = messageDigest.digest(input.getBytes(StandardCharsets.UTF_8)); + BigInteger number = new BigInteger(1, hashBytes); + String hexString = String.format(HEX_64_ZERO_PADDED_FORMAT, number); + return Base64.getEncoder().encodeToString(hexString.getBytes()); + } catch (NoSuchAlgorithmException e) { + throw ServiceException.FAILURE("Unable to encrypt", e); + } + } +}