From 7fb61717c2715c94f4a31b497081be4630c3f8c8 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 7 Aug 2025 23:20:37 +0200 Subject: [PATCH 1/4] refactor/update dependencies to exclude javax.activation and clean up code --- obp-api/pom.xml | 17 ++++++++++++++++- .../scala/code/model/dataAccess/AuthUser.scala | 1 - obp-commons/pom.xml | 10 ++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/obp-api/pom.xml b/obp-api/pom.xml index f2a4bd51b6..867840a7c5 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -101,7 +101,6 @@ commons-text 1.10.0 - org.apache.commons commons-email @@ -408,6 +407,12 @@ org.asynchttpclient async-http-client 2.10.4 + + + javax.activation + com.sun.activation + + @@ -416,6 +421,16 @@ org.scalikejdbc scalikejdbc_${scala.version} 3.4.0 + + + com.sun.activation + javax.activation + + + javax.activation + activation + + com.microsoft.sqlserver diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index daed891795..fa21830e27 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -597,7 +597,6 @@ import net.liftweb.util.Helpers._ val resetPasswordLinkProps = Constant.HostName val resetPasswordLink = APIUtil.getPropsValue("portal_hostname", resetPasswordLinkProps)+ passwordResetPath.mkString("/", "/", "/")+urlEncode(u.getUniqueId()) - logger.error("222222222222222222222222222222222222222444:"+classOf[javax.activation.DataSource].getProtectionDomain.getCodeSource) // Use Apache Commons Email wrapper instead of Lift Mailer val emailBodies = generateResetEmailBodies(u, resetPasswordLink) diff --git a/obp-commons/pom.xml b/obp-commons/pom.xml index c7f68bad46..b41909faf1 100644 --- a/obp-commons/pom.xml +++ b/obp-commons/pom.xml @@ -29,6 +29,16 @@ net.liftweb lift-util_${scala.version} + + + javax.activation + activation + + + javax.mail + mail + + net.liftweb From 328975f436884a2e8289c9c94ea99183686434a6 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 7 Aug 2025 23:20:48 +0200 Subject: [PATCH 2/4] refactor/update logging level in CommonsEmailWrapper to debug for email sending operations --- .../scala/code/api/util/CommonsEmailWrapper.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala index 3b57e97470..d836571108 100644 --- a/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala +++ b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala @@ -79,11 +79,11 @@ object CommonsEmailWrapper extends MdcLoggable { } /** - * Send simple text email + * Send a simple text email */ def sendTextEmail(config: EmailConfig, content: EmailContent): Box[String] = { try { - logger.info(s"Sending text email from ${content.from} to ${content.to.mkString(", ")}") + logger.debug(s"Sending text email from ${content.from} to ${content.to.mkString(", ")}") val email = new SimpleEmail() configureEmail(email, config, content) @@ -95,7 +95,7 @@ object CommonsEmailWrapper extends MdcLoggable { } val messageId = email.send() - logger.info(s"Email sent successfully with Message-ID: $messageId") + logger.debug(s"Email sent successfully with Message-ID: $messageId") Full(messageId) } catch { case e: Exception => @@ -109,7 +109,7 @@ object CommonsEmailWrapper extends MdcLoggable { */ def sendHtmlEmail(config: EmailConfig, content: EmailContent): Box[String] = { try { - logger.info(s"Sending HTML email from ${content.from} to ${content.to.mkString(", ")}") + logger.debug(s"Sending HTML email from ${content.from} to ${content.to.mkString(", ")}") val email = new HtmlEmail() configureEmail(email, config, content) @@ -124,7 +124,7 @@ object CommonsEmailWrapper extends MdcLoggable { content.textContent.foreach(email.setTextMsg) val messageId = email.send() - logger.info(s"HTML email sent successfully with Message-ID: $messageId") + logger.debug(s"HTML email sent successfully with Message-ID: $messageId") Full(messageId) } catch { case e: Exception => @@ -138,7 +138,7 @@ object CommonsEmailWrapper extends MdcLoggable { */ def sendEmailWithAttachments(config: EmailConfig, content: EmailContent): Box[String] = { try { - logger.info(s"Sending email with attachments from ${content.from} to ${content.to.mkString(", ")}") + logger.debug(s"Sending email with attachments from ${content.from} to ${content.to.mkString(", ")}") val email = new MultiPartEmail() configureEmail(email, config, content) @@ -150,7 +150,7 @@ object CommonsEmailWrapper extends MdcLoggable { content.attachments.foreach(email.attach) val messageId = email.send() - logger.info(s"Email with attachments sent successfully with Message-ID: $messageId") + logger.debug(s"Email with attachments sent successfully with Message-ID: $messageId") Full(messageId) } catch { case e: Exception => From a141dca5a7990171f48779cf2eaeec1499e011b8 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 8 Aug 2025 00:36:07 +0200 Subject: [PATCH 3/4] refactor/used jakarta Email instead of Apache Commons Email - step9 --- obp-api/pom.xml | 15 +- .../code/api/util/CommonsEmailWrapper.scala | 215 ++++++++---------- 2 files changed, 108 insertions(+), 122 deletions(-) diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 867840a7c5..9955f4ad7d 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -101,11 +101,6 @@ commons-text 1.10.0 - - org.apache.commons - commons-email - 1.5 - org.postgresql postgresql @@ -524,6 +519,16 @@ test + + com.sun.mail + jakarta.mail + 2.0.1 + + + jakarta.activation + jakarta.activation-api + 2.0.1 + com.sun.activation jakarta.activation diff --git a/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala index d836571108..f4cf89b56e 100644 --- a/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala +++ b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala @@ -1,21 +1,21 @@ package code.api.util import code.util.Helper.MdcLoggable +import jakarta.activation.{DataHandler, FileDataSource, URLDataSource} +import jakarta.mail._ +import jakarta.mail.internet._ import net.liftweb.common.{Box, Empty, Full} -import org.apache.commons.mail._ +import java.io.File import java.net.URL +import java.util.Properties /** - * Apache Commons Email Wrapper for OBP-API - * This wrapper provides a simple interface to send emails using Apache Commons Email - * instead of Lift Web's Mailer + * Jakarta Mail Wrapper for OBP-API + * This wrapper provides a simple interface to send emails using Jakarta Mail */ object CommonsEmailWrapper extends MdcLoggable { - /** - * Email configuration case class - */ case class EmailConfig( smtpHost: String, smtpPort: Int, @@ -24,12 +24,9 @@ object CommonsEmailWrapper extends MdcLoggable { useTLS: Boolean = true, useSSL: Boolean = false, debug: Boolean = false, - tlsProtocols: String = "TLSv1.2" // TLS protocols to use + tlsProtocols: String = "TLSv1.2" ) - /** - * Email content case class - */ case class EmailContent( from: String, to: List[String], @@ -41,9 +38,12 @@ object CommonsEmailWrapper extends MdcLoggable { attachments: List[EmailAttachment] = List.empty ) - /** - * Get default email configuration from OBP-API properties - */ + case class EmailAttachment( + filePath: Option[String] = None, + url: Option[String] = None, + name: Option[String] = None + ) + def getDefaultEmailConfig(): EmailConfig = { EmailConfig( smtpHost = APIUtil.getPropsValue("mail.smtp.host", "localhost"), @@ -57,46 +57,27 @@ object CommonsEmailWrapper extends MdcLoggable { ) } - /** - * Send simple text email with default configuration - */ def sendTextEmail(content: EmailContent): Box[String] = { sendTextEmail(getDefaultEmailConfig(), content) } - /** - * Send HTML email with default configuration - */ def sendHtmlEmail(content: EmailContent): Box[String] = { sendHtmlEmail(getDefaultEmailConfig(), content) } - /** - * Send email with attachments using default configuration - */ def sendEmailWithAttachments(content: EmailContent): Box[String] = { sendEmailWithAttachments(getDefaultEmailConfig(), content) } - /** - * Send a simple text email - */ def sendTextEmail(config: EmailConfig, content: EmailContent): Box[String] = { try { logger.debug(s"Sending text email from ${content.from} to ${content.to.mkString(", ")}") - - val email = new SimpleEmail() - configureEmail(email, config, content) - - // Set text content - content.textContent match { - case Some(text) => email.setMsg(text) - case None => email.setMsg("") - } - - val messageId = email.send() - logger.debug(s"Email sent successfully with Message-ID: $messageId") - Full(messageId) + val session = createSession(config) + val message = new MimeMessage(session) + setCommonHeaders(message, content) + message.setText(content.textContent.getOrElse(""), "UTF-8") + Transport.send(message) + Full(message.getMessageID) } catch { case e: Exception => logger.error(s"Failed to send text email: ${e.getMessage}", e) @@ -104,28 +85,28 @@ object CommonsEmailWrapper extends MdcLoggable { } } - /** - * Send HTML email - */ def sendHtmlEmail(config: EmailConfig, content: EmailContent): Box[String] = { try { logger.debug(s"Sending HTML email from ${content.from} to ${content.to.mkString(", ")}") - - val email = new HtmlEmail() - configureEmail(email, config, content) - - // Set HTML content - content.htmlContent match { - case Some(html) => email.setHtmlMsg(html) - case None => email.setHtmlMsg("No content") + val session = createSession(config) + val message = new MimeMessage(session) + setCommonHeaders(message, content) + val multipart = { + new MimeMultipart("alternative") + } + content.textContent.foreach { text => + val textPart = new MimeBodyPart() + textPart.setText(text, "UTF-8") + multipart.addBodyPart(textPart) + } + content.htmlContent.foreach { html => + val htmlPart = new MimeBodyPart() + htmlPart.setContent(html, "text/html; charset=UTF-8") + multipart.addBodyPart(htmlPart) } - - // Set text content as fallback - content.textContent.foreach(email.setTextMsg) - - val messageId = email.send() - logger.debug(s"HTML email sent successfully with Message-ID: $messageId") - Full(messageId) + message.setContent(multipart) + Transport.send(message) + Full(message.getMessageID) } catch { case e: Exception => logger.error(s"Failed to send HTML email: ${e.getMessage}", e) @@ -133,25 +114,45 @@ object CommonsEmailWrapper extends MdcLoggable { } } - /** - * Send email with attachments - */ def sendEmailWithAttachments(config: EmailConfig, content: EmailContent): Box[String] = { try { logger.debug(s"Sending email with attachments from ${content.from} to ${content.to.mkString(", ")}") - - val email = new MultiPartEmail() - configureEmail(email, config, content) - - // Set text content - content.textContent.foreach(email.setMsg) - + val session = createSession(config) + val message = new MimeMessage(session) + setCommonHeaders(message, content) + val multipart = new MimeMultipart() + // Add text or HTML part + (content.htmlContent, content.textContent) match { + case (Some(html), _) => + val htmlPart = new MimeBodyPart() + htmlPart.setContent(html, "text/html; charset=UTF-8") + multipart.addBodyPart(htmlPart) + case (None, Some(text)) => + val textPart = new MimeBodyPart() + textPart.setText(text, "UTF-8") + multipart.addBodyPart(textPart) + case _ => + val textPart = new MimeBodyPart() + textPart.setText("", "UTF-8") + multipart.addBodyPart(textPart) + } // Add attachments - content.attachments.foreach(email.attach) - - val messageId = email.send() - logger.debug(s"Email with attachments sent successfully with Message-ID: $messageId") - Full(messageId) + content.attachments.foreach { att => + val attachPart = new MimeBodyPart() + if (att.filePath.isDefined) { + val fds = new FileDataSource(new File(att.filePath.get)) + attachPart.setDataHandler(new DataHandler(fds)) + attachPart.setFileName(att.name.getOrElse(new File(att.filePath.get).getName)) + } else if (att.url.isDefined) { + val uds = new URLDataSource(new URL(att.url.get)) + attachPart.setDataHandler(new DataHandler(uds)) + attachPart.setFileName(att.name.getOrElse(att.url.get.split('/').last)) + } + multipart.addBodyPart(attachPart) + } + message.setContent(multipart) + Transport.send(message) + Full(message.getMessageID) } catch { case e: Exception => logger.error(s"Failed to send email with attachments: ${e.getMessage}", e) @@ -159,54 +160,34 @@ object CommonsEmailWrapper extends MdcLoggable { } } - /** - * Configure email with common settings - */ - private def configureEmail(email: Email, config: EmailConfig, content: EmailContent): Unit = { - // SMTP Configuration - email.setHostName(config.smtpHost) - email.setSmtpPort(config.smtpPort) - email.setAuthenticator(new DefaultAuthenticator(config.username, config.password)) - email.setSSLOnConnect(config.useSSL) - email.setStartTLSEnabled(config.useTLS) - email.setDebug(config.debug) - email.getMailSession.getProperties.setProperty("mail.smtp.ssl.protocols", config.tlsProtocols) - - // Set charset - email.setCharset("UTF-8") - - // Set sender - email.setFrom(content.from) - - // Set recipients - content.to.foreach(email.addTo) - content.cc.foreach(email.addCc) - content.bcc.foreach(email.addBcc) - - // Set subject - email.setSubject(content.subject) + private def createSession(config: EmailConfig): Session = { + val props = new Properties() + props.put("mail.smtp.host", config.smtpHost) + props.put("mail.smtp.port", config.smtpPort.toString) + props.put("mail.smtp.auth", "true") + props.put("mail.smtp.starttls.enable", config.useTLS.toString) + props.put("mail.smtp.ssl.enable", config.useSSL.toString) + props.put("mail.debug", config.debug.toString) + props.put("mail.smtp.ssl.protocols", config.tlsProtocols) + val authenticator = new Authenticator() { + override def getPasswordAuthentication: PasswordAuthentication = + new PasswordAuthentication(config.username, config.password) + } + Session.getInstance(props, authenticator) } - /** - * Create email attachment from file - */ - def createFileAttachment(filePath: String, name: Option[String] = None): EmailAttachment = { - val attachment = new EmailAttachment() - attachment.setPath(filePath) - attachment.setDisposition(EmailAttachment.ATTACHMENT) - name.foreach(attachment.setName) - attachment + private def setCommonHeaders(message: MimeMessage, content: EmailContent): Unit = { + message.setFrom(new InternetAddress(content.from)) + content.to.foreach(addr => message.addRecipient(Message.RecipientType.TO, new InternetAddress(addr))) + content.cc.foreach(addr => message.addRecipient(Message.RecipientType.CC, new InternetAddress(addr))) + content.bcc.foreach(addr => message.addRecipient(Message.RecipientType.BCC, new InternetAddress(addr))) + message.setSubject(content.subject, "UTF-8") } - /** - * Create email attachment from URL - */ - def createUrlAttachment(url: String, name: String): EmailAttachment = { - val attachment = new EmailAttachment() - attachment.setURL(new URL(url)) - attachment.setDisposition(EmailAttachment.ATTACHMENT) - attachment.setName(name) - attachment - } + def createFileAttachment(filePath: String, name: Option[String] = None): EmailAttachment = + EmailAttachment(filePath = Some(filePath), url = None, name = name) + + def createUrlAttachment(url: String, name: String): EmailAttachment = + EmailAttachment(filePath = None, url = Some(url), name = Some(name)) } \ No newline at end of file From 9fdfa7e34cd24a9c0ce14f2b6f6443ee3682394d Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 8 Aug 2025 00:44:08 +0200 Subject: [PATCH 4/4] refactor/used jakarta Email instead of Apache Commons Email - step10 replaced Lift Mailer with JakartaMail for password reset and validation emails, simplifying email content generation --- .../code/model/dataAccess/AuthUser.scala | 36 ++++--------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index fa21830e27..0d9334ff58 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -589,23 +589,14 @@ import net.liftweb.util.Helpers._ */ override def sendPasswordReset(name: String) { findAuthUserByUsernameLocallyLegacy(name).toList ::: findUsersByEmailLocally(name) map { - // reason of case parameter name is "u" instead of "user": trait AuthUser have constant mumber name is "user" - // So if the follow case paramter name is "user" will cause compile warnings case u if u.validated_? => u.resetUniqueId().save - //NOTE: here, if server_mode = portal, so we need modify the resetLink to portal_hostname, then developer can get proper response.. val resetPasswordLinkProps = Constant.HostName val resetPasswordLink = APIUtil.getPropsValue("portal_hostname", resetPasswordLinkProps)+ passwordResetPath.mkString("/", "/", "/")+urlEncode(u.getUniqueId()) - // Use Apache Commons Email wrapper instead of Lift Mailer - val emailBodies = generateResetEmailBodies(u, resetPasswordLink) - - // Extract text and HTML content from email bodies - val textContent = emailBodies.find(_.isInstanceOf[net.liftweb.util.Mailer.PlainMailBodyType]) - .map(_.asInstanceOf[net.liftweb.util.Mailer.PlainMailBodyType].toString.replace("PlainMailBodyType(", "").replace(")", "")) - val htmlContent = emailBodies.find(_.isInstanceOf[net.liftweb.util.Mailer.XHTMLMailBodyType]) - .map(_.asInstanceOf[net.liftweb.util.Mailer.XHTMLMailBodyType].toString.replace("XHTMLMailBodyType(", "").replace(")", "")) - + // Directly generate content using JakartaMail/CommonsEmailWrapper + val textContent = Some(s"Please use the following link to reset your password: $resetPasswordLink") + val htmlContent = Some(s"

Please use the following link to reset your password:

$resetPasswordLink

") val emailContent = EmailContent( from = emailFrom, to = List(u.getEmail), @@ -627,8 +618,6 @@ import net.liftweb.util.Helpers._ case u => sendValidationEmail(u) } - // In order to prevent any leakage of information we use the same message for all cases - // Note: Individual success/error messages are now handled in the email sending logic above } override def lostPasswordXhtml = { @@ -660,22 +649,10 @@ import net.liftweb.util.Helpers._ * Overridden to use the hostname set in the props file */ override def sendValidationEmail(user: TheUserType) { - val resetLink = Constant.HostName+"/"+validateUserPath.mkString("/")+ - "/"+urlEncode(user.getUniqueId()) - + val resetLink = Constant.HostName+"/"+validateUserPath.mkString("/")+"/"+urlEncode(user.getUniqueId()) val email: String = user.getEmail - - val msgXml = signupMailBody(user, resetLink) - - // Use Apache Commons Email wrapper instead of Lift Mailer - val emailBodies: List[Mailer.MailBodyType] = generateValidationEmailBodies(user, resetLink) - - // Extract text and HTML content from email bodies - val textContent = emailBodies.find(_.isInstanceOf[net.liftweb.util.Mailer.PlainMailBodyType]) - .map(_.asInstanceOf[net.liftweb.util.Mailer.PlainMailBodyType].toString.replace("PlainMailBodyType(", "").replace(")", "")) - val htmlContent = emailBodies.find(_.isInstanceOf[net.liftweb.util.Mailer.XHTMLMailBodyType]) - .map(_.asInstanceOf[net.liftweb.util.Mailer.XHTMLMailBodyType].toString.replace("XHTMLMailBodyType(", "").replace(")", "")) - + val textContent = Some(s"Welcome! Please validate your account by clicking the following link: $resetLink") + val htmlContent = Some(s"

Welcome! Please validate your account by clicking the following link:

$resetLink

") val emailContent = EmailContent( from = emailFrom, to = List(user.getEmail), @@ -684,7 +661,6 @@ import net.liftweb.util.Helpers._ textContent = textContent, htmlContent = htmlContent ) - sendHtmlEmail(emailContent) match { case Full(messageId) => logger.debug(s"Validation email sent successfully with Message-ID: $messageId")