com.sun.activation
javax.activation
diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template
index 64c7733d3..e430d6553 100644
--- a/obp-api/src/main/resources/props/sample.props.template
+++ b/obp-api/src/main/resources/props/sample.props.template
@@ -165,6 +165,10 @@ jwt.use.ssl=false
# Bypass TPP signature validation
# bypass_tpp_signature_validation = false
+## Use TPP signature revocation list
+## - CRLs (Certificate Revocation Lists), or
+## - OCSP (Online Certificate Status Protocol).
+# use_tpp_signature_revocation_list = true
## Reject Berlin Group TRANSACTIONS with status "received" after a defined time (in seconds)
# berlin_group_outdated_transactions_time_in_seconds = 300
@@ -266,20 +270,59 @@ dev.port=8080
#Default value is obp (very highly recomended)
apiPathZero=obp
-## Sending mail out
-## Not need in dev mode, but important for production
-mail.api.consumer.registered.sender.address=no-reply@example.com
-mail.api.consumer.registered.notification.addresses=you@example.com
-## Not need in dev mode, but important for production
-## We send an email after any exception
-# mail.exception.sender.address=no-reply@example.com
-# mail.exception.registered.notification.addresses=notify@example.com,notify2@example.com,notify3@example.com
-# This property allows sending API registration data to developer's email.
-#mail.api.consumer.registered.notification.send=false
-We only send consumer keys and secret if this is true
-#mail.api.consumer.registered.notification.send.sensistive=false
+## Email Configuration (CommonsEmailWrapper)
+## ===========================================
+##
+## This section configures email sending using CommonsEmailWrapper instead of Lift Mailer.
+## All email functionality (password reset, validation, notifications) now uses these settings.
+##
+## SMTP Server Configuration
+## -------------------------
+## Basic SMTP settings
mail.smtp.host=127.0.0.1
mail.smtp.port=25
+mail.smtp.auth=false
+mail.smtp.user=
+mail.smtp.password=
+
+## TLS/SSL Configuration
+## ---------------------
+## Enable STARTTLS (recommended for most SMTP servers)
+mail.smtp.starttls.enable=false
+## Enable SSL (use with port 465)
+mail.smtp.ssl.enable=false
+## TLS protocols to use (recommended: TLSv1.2)
+mail.smtp.ssl.protocols=TLSv1.2
+## Trust all certificates (for development only)
+#mail.smtp.ssl.trust=*
+
+## Debug Configuration
+## ------------------
+## Enable email debugging (shows SMTP communication)
+mail.debug=false
+
+## Email Sender Configuration
+## -------------------------
+## Default sender address for all emails
+mail.users.userinfo.sender.address=no-reply@example.com
+
+## Consumer Registration Email
+## --------------------------
+## Enable/disable consumer registration notifications
+mail.api.consumer.registered.notification.send=false
+## Sender address for consumer registration emails
+mail.api.consumer.registered.sender.address=no-reply@example.com
+## Recipient addresses for consumer registration notifications (comma-separated)
+mail.api.consumer.registered.notification.addresses=you@example.com
+## Send sensitive information (consumer keys/secrets) via email
+mail.api.consumer.registered.notification.send.sensistive=false
+
+## Exception Notification Email
+## ---------------------------
+## Sender address for exception notifications
+mail.exception.sender.address=no-reply@example.com
+## Recipient addresses for exception notifications (comma-separated)
+mail.exception.registered.notification.addresses=notify@example.com,notify2@example.com,notify3@example.com
## Oauth token timeout
token_expiration_weeks=4
diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
index 3f607ecc8..9ae42172e 100644
--- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
+++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
@@ -46,6 +46,7 @@ import code.api.util.ApiRole.CanCreateEntitlementAtAnyBank
import code.api.util.ErrorMessages.MandatoryPropertyIsNotSet
import code.api.util._
import code.api.util.migration.Migration
+import code.api.util.CommonsEmailWrapper
import code.api.util.migration.Migration.DbFunction
import code.apicollection.ApiCollection
import code.apicollectionendpoint.ApiCollectionEndpoint
@@ -151,7 +152,6 @@ import org.apache.commons.io.FileUtils
import java.io.{File, FileInputStream}
import java.util.stream.Collectors
import java.util.{Locale, TimeZone}
-import javax.mail.internet.MimeMessage
import scala.concurrent.ExecutionContext
/**
@@ -693,10 +693,6 @@ class Boot extends MdcLoggable {
case e: ExceptionInInitializerError => logger.warn(s"BankAccountCreationListener Exception: $e")
}
- Mailer.devModeSend.default.set( (m : MimeMessage) => {
- logger.info("Would have sent email if not in dev mode: " + m.getContent)
- })
-
LiftRules.exceptionHandler.prepend{
case(_, r, e) if e.isInstanceOf[NullPointerException] && e.getMessage.contains("Looking for Connection Identifier") => {
logger.error(s"Exception being returned to browser when processing url is ${r.request.uri}, method is ${r.request.method}, exception detail is $e", e)
@@ -880,7 +876,7 @@ class Boot extends MdcLoggable {
}
private def sendExceptionEmail(exception: Throwable): Unit = {
- import Mailer.{From, PlainMailBodyType, Subject, To}
+
import net.liftweb.util.Helpers.now
val outputStream = new java.io.ByteArrayOutputStream
@@ -899,18 +895,18 @@ class Boot extends MdcLoggable {
//technically doesn't work for all valid email addresses so this will mess up if someone tries to send emails to "foo,bar"@example.com
val to = toAddressesString.split(",").toList
- val toParams = to.map(To(_))
- val params = PlainMailBodyType(error) :: toParams
- //this is an async call
- Mailer.sendMail(
- From(from),
- Subject(s"you got an exception on $host"),
- params :_*
+ val emailContent = CommonsEmailWrapper.EmailContent(
+ from = from,
+ to = to,
+ subject = s"you got an exception on $host",
+ textContent = Some(error)
)
+
+ //this is an async call∆∆
+ CommonsEmailWrapper.sendTextEmail(emailContent)
}
- //if Mailer.sendMail wasn't called (note: this actually isn't checking if the mail failed to send as that is being done asynchronously)
if(mailSent.isEmpty)
logger.warn(s"Exception notification failed: $mailSent")
}
diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala
index a464be47f..1d0a742e2 100644
--- a/obp-api/src/main/scala/code/api/OAuth2.scala
+++ b/obp-api/src/main/scala/code/api/OAuth2.scala
@@ -410,10 +410,21 @@ object OAuth2Login extends RestHelper with MdcLoggable {
validateAccessToken(token) match {
case Full(_) =>
val user = getOrCreateResourceUser(token)
- val consumer = getOrCreateConsumer(token, user.map(_.userId), Some("OAuth 2.0"))
- LoginAttempt.userIsLocked(user.map(_.provider).getOrElse(""), user.map(_.name).getOrElse("")) match {
- case true => ((Failure(UsernameHasBeenLocked), Some(cc.copy(consumer = consumer))))
- case false => (user, Some(cc.copy(consumer = consumer)))
+ val consumer: Box[Consumer] = getOrCreateConsumer(token, user.map(_.userId), Some("OAuth 2.0"))
+ consumer match {
+ case Full(_) =>
+ LoginAttempt.userIsLocked(user.map(_.provider).getOrElse(""), user.map(_.name).getOrElse("")) match {
+ case true => ((Failure(UsernameHasBeenLocked), Some(cc.copy(consumer = consumer))))
+ case false => (user, Some(cc.copy(consumer = consumer)))
+ }
+ case ParamFailure(msg, exception, chain, apiFailure: APIFailure) =>
+ logger.debug(s"ParamFailure - message: $msg, param: $apiFailure, exception: ${exception.map(_.getMessage).openOr("none")}, chain: ${chain.map(_.msg).openOr("none")}")
+ (ParamFailure(msg, exception, chain, apiFailure: APIFailure), Some(cc))
+ case Failure(msg, exception, c) =>
+ logger.error(s"Failure - message: $msg, exception: ${exception.map(_.getMessage).openOr("none")}")
+ (Failure(msg, exception, c), Some(cc))
+ case _ =>
+ (Failure(CreateConsumerError), Some(cc))
}
case ParamFailure(a, b, c, apiFailure: APIFailure) =>
(ParamFailure(a, b, c, apiFailure: APIFailure), Some(cc))
diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala
index e7dc68c5c..2decaab96 100644
--- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala
+++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/MessageDocsSwaggerDefinitions.scala
@@ -232,7 +232,7 @@ object MessageDocsSwaggerDefinitions
startDate = DateWithDayExampleObject,
finishDate = Some(DateWithDayExampleObject),
balance = BigDecimal(balanceAmountExample.value),
- status = transactionStatusExample.value,
+ status = Some(transactionStatusExample.value),
)
val accountRouting = AccountRouting("","")
diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala
index bba14b21c..d3684b268 100644
--- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala
+++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala
@@ -1036,17 +1036,27 @@ Give detailed information about the addressed account together with balance info
cc =>
for {
(Full(u), callContext) <- authenticatedAccess(cc)
+ withBalanceParam <- NewStyle.function.tryons(s"$InvalidUrlParameters withBalance parameter can only take two values: TRUE or FALSE!", 400, callContext) {
+ val withBalance = APIUtil.getHttpRequestUrlParam(cc.url, "withBalance")
+ if (withBalance.isEmpty) Some(false) else Some(withBalance.toBoolean)
+ }
_ <- passesPsd2Aisp(callContext)
(account: BankAccount, callContext) <- NewStyle.function.getBankAccountByAccountId(AccountId(accountId), callContext)
(canReadBalancesAccounts, callContext) <- NewStyle.function.getAccountCanReadBalancesOfBerlinGroup(u, callContext)
(canReadTransactionsAccounts, callContext) <- NewStyle.function.getAccountCanReadTransactionsOfBerlinGroup(u, callContext)
_ <- checkAccountAccess(ViewId(SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID), u, account, callContext)
+ (accountBalances, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.getBankAccountBalances(
+ AccountId(accountId),
+ callContext
+ )
} yield {
(
JSONFactory_BERLIN_GROUP_1_3.createAccountDetailsJson(
account,
canReadBalancesAccounts,
canReadTransactionsAccounts,
+ withBalanceParam,
+ accountBalances,
u
),
callContext
@@ -1105,8 +1115,23 @@ respectively the OAuth2 access token.
(canReadBalancesAccounts, callContext) <- NewStyle.function.getAccountCanReadBalancesOfBerlinGroup(u, callContext)
(canReadTransactionsAccounts, callContext) <- NewStyle.function.getAccountCanReadTransactionsOfBerlinGroup(u, callContext)
_ <- checkAccountAccess(ViewId(SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID), u, account, callContext)
+ withBalanceParam <- NewStyle.function.tryons(s"$InvalidUrlParameters withBalance parameter can only take two values: TRUE or FALSE!", 400, callContext) {
+ val withBalance = APIUtil.getHttpRequestUrlParam(cc.url, "withBalance")
+ if (withBalance.isEmpty) Some(false) else Some(withBalance.toBoolean)
+ }
+ (accountBalances, callContext) <- code.api.util.newstyle.BankAccountBalanceNewStyle.getBankAccountBalances(
+ AccountId(accountId),
+ callContext
+ )
} yield {
- (JSONFactory_BERLIN_GROUP_1_3.createCardAccountDetailsJson(account, canReadBalancesAccounts, canReadTransactionsAccounts, u), callContext)
+ (JSONFactory_BERLIN_GROUP_1_3.createCardAccountDetailsJson(
+ account,
+ canReadBalancesAccounts,
+ canReadTransactionsAccounts,
+ withBalanceParam,
+ accountBalances,
+ u
+ ), callContext)
}
}
}
diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala
index f7857a00d..52d78bfb3 100644
--- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala
+++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala
@@ -93,6 +93,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
product: String,
cashAccountType: String,
name: Option[String],
+ balances: Option[List[CoreAccountBalanceJson]] = None,
_links: AccountDetailsLinksJsonV13,
)
@@ -407,14 +408,18 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
def createCardAccountDetailsJson(bankAccount: BankAccount,
canReadBalancesAccounts: List[BankIdAccountId],
canReadTransactionsAccounts: List[BankIdAccountId],
+ withBalanceParam: Option[Boolean],
+ balances: List[BankAccountBalanceTrait],
user: User): CardAccountDetailsJsonV13 = {
- val accountDetailsJsonV13 = createAccountDetailsJson(bankAccount, canReadBalancesAccounts, canReadTransactionsAccounts, user)
+ val accountDetailsJsonV13 = createAccountDetailsJson(bankAccount, canReadBalancesAccounts, canReadTransactionsAccounts, withBalanceParam, balances, user)
CardAccountDetailsJsonV13(accountDetailsJsonV13.account)
}
def createAccountDetailsJson(bankAccount: BankAccount,
canReadBalancesAccounts: List[BankIdAccountId],
canReadTransactionsAccounts: List[BankIdAccountId],
+ withBalanceParam: Option[Boolean],
+ balances: List[BankAccountBalanceTrait],
user: User): AccountDetailsJsonV13 = {
val (iBan: String, bBan: String) = getIbanAndBban(bankAccount)
val commonPath = s"${OBP_BERLIN_GROUP_1_3.apiVersion.urlPrefix}/${OBP_BERLIN_GROUP_1_3.version}/accounts/${bankAccount.accountId.value}"
@@ -423,7 +428,15 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
val transactionRef = LinkHrefJson(s"/$commonPath/transactions")
val canReadTransactions = canReadTransactionsAccounts.map(_.accountId.value).contains(bankAccount.accountId.value)
val cashAccountType = bankAccount.attributes.getOrElse(Nil).filter(_.name== "cashAccountType").map(_.value).headOption.getOrElse("")
-
+ val accountBalances = if (withBalanceParam.contains(true)) {
+ Some(balances.filter(_.accountId.equals(bankAccount.accountId)).flatMap(balance => (List(CoreAccountBalanceJson(
+ balanceAmount = AmountOfMoneyV13(bankAccount.currency, balance.balanceAmount.toString()),
+ balanceType = balance.balanceType,
+ lastChangeDateTime = balance.lastChangeDateTime.map(APIUtil.DateWithMsAndTimeZoneOffset.format(_))
+ )))))
+ } else {
+ None
+ }
val account = AccountJsonV13(
resourceId = bankAccount.accountId.value,
@@ -432,6 +445,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats with MdcLoggable{
name = if(APIUtil.getPropsAsBoolValue("BG_v1312_show_account_name", defaultValue = true)) Some(bankAccount.name) else None,
cashAccountType = cashAccountType,
product = bankAccount.accountType,
+ balances = if(canReadBalances) accountBalances else None,
_links = AccountDetailsLinksJsonV13(
balances = if (canReadBalances) Some(balanceRef) else None,
transactions = if (canReadTransactions) Some(transactionRef) else None,
diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala
index db5049469..176726d85 100644
--- a/obp-api/src/main/scala/code/api/util/APIUtil.scala
+++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala
@@ -71,7 +71,7 @@ import code.util.{Helper, JsonSchemaUtil}
import code.views.system.AccountAccess
import code.views.{MapperViews, Views}
import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue
-import com.alibaba.ttl.internal.javassist.CannotCompileException
+import javassist.CannotCompileException
import com.github.dwickern.macros.NameOf.{nameOf, nameOfType}
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.model._
@@ -1147,6 +1147,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
case "iss" => Full(OBPIss(values.head))
case "consent_id" => Full(OBPConsentId(values.head))
case "user_id" => Full(OBPUserId(values.head))
+ case "provider_provider_id" => Full(ProviderProviderId(values.head))
case "bank_id" => Full(OBPBankId(values.head))
case "account_id" => Full(OBPAccountId(values.head))
case "url" => Full(OBPUrl(values.head))
@@ -1198,6 +1199,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
iss <- getHttpParamValuesByName(httpParams,"iss")
consentId <- getHttpParamValuesByName(httpParams,"consent_id")
userId <- getHttpParamValuesByName(httpParams, "user_id")
+ providerProviderId <- getHttpParamValuesByName(httpParams, "provider_provider_id")
bankId <- getHttpParamValuesByName(httpParams, "bank_id")
accountId <- getHttpParamValuesByName(httpParams, "account_id")
url <- getHttpParamValuesByName(httpParams, "url")
@@ -1231,9 +1233,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
*/
//val sortBy = json.header("obp_sort_by")
val ordering = OBPOrdering(None, sortDirection)
- //This guarantee the order
+ //This guarantee the order
List(limit, offset, ordering, sortBy, fromDate, toDate,
- anon, status, consumerId, azp, iss, consentId, userId, url, appName, implementedByPartialFunction, implementedInVersion,
+ anon, status, consumerId, azp, iss, consentId, userId, providerProviderId, url, appName, implementedByPartialFunction, implementedInVersion,
verb, correlationId, duration, excludeAppNames, excludeUrlPattern, excludeImplementedByPartialfunctions,
includeAppNames, includeUrlPattern, includeImplementedByPartialfunctions,
connectorName,functionName, bankId, accountId, customerId, lockedStatus, deletedStatus
@@ -1276,6 +1278,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
val azp = getHttpRequestUrlParam(httpRequestUrl,"azp")
val consentId = getHttpRequestUrlParam(httpRequestUrl,"consent_id")
val userId = getHttpRequestUrlParam(httpRequestUrl, "user_id")
+ val providerProviderId = getHttpRequestUrlParam(httpRequestUrl, "provider_provider_id")
val bankId = getHttpRequestUrlParam(httpRequestUrl, "bank_id")
val accountId = getHttpRequestUrlParam(httpRequestUrl, "account_id")
val url = getHttpRequestUrlParam(httpRequestUrl, "url")
@@ -1305,7 +1308,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
Full(List(
HTTPParam("sort_by",sortBy), HTTPParam("sort_direction",sortDirection), HTTPParam("from_date",fromDate), HTTPParam("to_date", toDate), HTTPParam("limit",limit), HTTPParam("offset",offset),
- HTTPParam("anon", anon), HTTPParam("status", status), HTTPParam("consumer_id", consumerId), HTTPParam("azp", azp), HTTPParam("iss", iss), HTTPParam("consent_id", consentId), HTTPParam("user_id", userId), HTTPParam("url", url), HTTPParam("app_name", appName),
+ HTTPParam("anon", anon), HTTPParam("status", status), HTTPParam("consumer_id", consumerId), HTTPParam("azp", azp), HTTPParam("iss", iss), HTTPParam("consent_id", consentId), HTTPParam("user_id", userId), HTTPParam("provider_provider_id", providerProviderId), HTTPParam("url", url), HTTPParam("app_name", appName),
HTTPParam("implemented_by_partial_function",implementedByPartialFunction), HTTPParam("implemented_in_version",implementedInVersion), HTTPParam("verb", verb),
HTTPParam("correlation_id", correlationId), HTTPParam("duration", duration), HTTPParam("exclude_app_names", excludeAppNames),
HTTPParam("exclude_url_patterns", excludeUrlPattern),HTTPParam("exclude_implemented_by_partial_functions", excludeImplementedByPartialfunctions),
@@ -3020,6 +3023,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
// Identify consumer via certificate
val consumerByCertificate = Consent.getCurrentConsumerViaTppSignatureCertOrMtls(callContext = cc)
+ logger.debug(s"consumerByCertificate: $consumerByCertificate")
val method = APIUtil.getPropsValue(nameOfProperty = "consumer_validation_method_for_consent", defaultValue = "CONSUMER_CERTIFICATE")
val consumerByConsumerKey = getConsumerKey(reqHeaders) match {
case Some(consumerKey) if method == "CONSUMER_KEY_VALUE" =>
@@ -3027,6 +3031,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
case None =>
Empty
}
+ logger.debug(s"consumerByConsumerKey: $consumerByConsumerKey")
val res =
if (authHeadersWithEmptyValues.nonEmpty) { // Check Authorization Headers Empty Values
diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala
index 019aabd6e..4cc0a408f 100644
--- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala
+++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala
@@ -80,7 +80,11 @@ object CertificateVerifier extends MdcLoggable {
// Set up PKIX parameters for validation
val pkixParams = new PKIXParameters(trustAnchors)
- pkixParams.setRevocationEnabled(false) // Disable CRL checks
+ if(APIUtil.getPropsAsBoolValue("use_tpp_signature_revocation_list", defaultValue = true)) {
+ pkixParams.setRevocationEnabled(true) // Enable CRL checks
+ } else {
+ pkixParams.setRevocationEnabled(false) // Disable CRL checks
+ }
// Validate certificate chain
val certPath = CertificateFactory.getInstance("X.509").generateCertPath(Collections.singletonList(certificate))
diff --git a/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala
new file mode 100644
index 000000000..f4cf89b56
--- /dev/null
+++ b/obp-api/src/main/scala/code/api/util/CommonsEmailWrapper.scala
@@ -0,0 +1,193 @@
+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 java.io.File
+import java.net.URL
+import java.util.Properties
+
+/**
+ * Jakarta Mail Wrapper for OBP-API
+ * This wrapper provides a simple interface to send emails using Jakarta Mail
+ */
+object CommonsEmailWrapper extends MdcLoggable {
+
+ case class EmailConfig(
+ smtpHost: String,
+ smtpPort: Int,
+ username: String,
+ password: String,
+ useTLS: Boolean = true,
+ useSSL: Boolean = false,
+ debug: Boolean = false,
+ tlsProtocols: String = "TLSv1.2"
+ )
+
+ case class EmailContent(
+ from: String,
+ to: List[String],
+ cc: List[String] = List.empty,
+ bcc: List[String] = List.empty,
+ subject: String,
+ textContent: Option[String] = None,
+ htmlContent: Option[String] = None,
+ attachments: List[EmailAttachment] = List.empty
+ )
+
+ 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"),
+ smtpPort = APIUtil.getPropsValue("mail.smtp.port", "1025").toInt,
+ username = APIUtil.getPropsValue("mail.smtp.user", ""),
+ password = APIUtil.getPropsValue("mail.smtp.password", ""),
+ useTLS = APIUtil.getPropsValue("mail.smtp.starttls.enable", "false").toBoolean,
+ useSSL = APIUtil.getPropsValue("mail.smtp.ssl.enable", "false").toBoolean,
+ debug = APIUtil.getPropsValue("mail.debug", "false").toBoolean,
+ tlsProtocols = APIUtil.getPropsValue("mail.smtp.ssl.protocols", "TLSv1.2")
+ )
+ }
+
+ def sendTextEmail(content: EmailContent): Box[String] = {
+ sendTextEmail(getDefaultEmailConfig(), content)
+ }
+
+ def sendHtmlEmail(content: EmailContent): Box[String] = {
+ sendHtmlEmail(getDefaultEmailConfig(), content)
+ }
+
+ def sendEmailWithAttachments(content: EmailContent): Box[String] = {
+ sendEmailWithAttachments(getDefaultEmailConfig(), content)
+ }
+
+ def sendTextEmail(config: EmailConfig, content: EmailContent): Box[String] = {
+ try {
+ logger.debug(s"Sending text email from ${content.from} to ${content.to.mkString(", ")}")
+ 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)
+ Empty
+ }
+ }
+
+ def sendHtmlEmail(config: EmailConfig, content: EmailContent): Box[String] = {
+ try {
+ logger.debug(s"Sending HTML email from ${content.from} to ${content.to.mkString(", ")}")
+ 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)
+ }
+ message.setContent(multipart)
+ Transport.send(message)
+ Full(message.getMessageID)
+ } catch {
+ case e: Exception =>
+ logger.error(s"Failed to send HTML email: ${e.getMessage}", e)
+ Empty
+ }
+ }
+
+ def sendEmailWithAttachments(config: EmailConfig, content: EmailContent): Box[String] = {
+ try {
+ logger.debug(s"Sending email with attachments from ${content.from} to ${content.to.mkString(", ")}")
+ 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 { 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)
+ Empty
+ }
+ }
+
+ 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)
+ }
+
+ 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")
+ }
+
+ 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
diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala
index cad5fa3c2..937c892de 100644
--- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala
+++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala
@@ -238,6 +238,7 @@ object Consent extends MdcLoggable {
private def tppIsConsentHolder(consumerIdFromConsent: String, callContext: CallContext): Boolean = {
val consumerIdFromCurrentCall = callContext.consumer.map(_.consumerId.get).orNull
+ logger.debug(s"consumerIdFromConsent == consumerIdFromCurrentCall ($consumerIdFromConsent == $consumerIdFromCurrentCall)")
consumerIdFromConsent == consumerIdFromCurrentCall
}
diff --git a/obp-api/src/main/scala/code/api/util/NotificationUtil.scala b/obp-api/src/main/scala/code/api/util/NotificationUtil.scala
index 8b8f95e4a..cc2e52160 100644
--- a/obp-api/src/main/scala/code/api/util/NotificationUtil.scala
+++ b/obp-api/src/main/scala/code/api/util/NotificationUtil.scala
@@ -6,8 +6,7 @@ import code.users.Users
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.model.User
import net.liftweb.common.Box
-import net.liftweb.util.Mailer
-import net.liftweb.util.Mailer._
+
import scala.collection.immutable.List
@@ -27,14 +26,14 @@ object NotificationUtil extends MdcLoggable {
|
|Cheers
|""".stripMargin
- val params = PlainMailBodyType(bodyOfMessage) :: List(To(user.emailAddress))
- val subjectOfMessage = "You have been granted the role"
- //this is an async call
- Mailer.sendMail(
- From(from),
- Subject(subjectOfMessage),
- params :_*
+ val emailContent = CommonsEmailWrapper.EmailContent(
+ from = from,
+ to = List(user.emailAddress),
+ subject = s"You have been granted the role: ${entitlement.roleName}",
+ textContent = Some(bodyOfMessage)
)
+ //this is an async call
+ CommonsEmailWrapper.sendTextEmail(emailContent)
}
if(mailSent.isEmpty) {
val info =
diff --git a/obp-api/src/main/scala/code/api/util/OBPParam.scala b/obp-api/src/main/scala/code/api/util/OBPParam.scala
index 49bd62193..bc42c0465 100644
--- a/obp-api/src/main/scala/code/api/util/OBPParam.scala
+++ b/obp-api/src/main/scala/code/api/util/OBPParam.scala
@@ -31,6 +31,7 @@ case class OBPAzp(value: String) extends OBPQueryParam
case class OBPIss(value: String) extends OBPQueryParam
case class OBPConsentId(value: String) extends OBPQueryParam
case class OBPUserId(value: String) extends OBPQueryParam
+case class ProviderProviderId(value: String) extends OBPQueryParam
case class OBPStatus(value: String) extends OBPQueryParam
case class OBPBankId(value: String) extends OBPQueryParam
case class OBPAccountId(value: String) extends OBPQueryParam
diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala
index 182f39a70..7da81d82c 100644
--- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala
+++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala
@@ -100,6 +100,7 @@ object Migration extends MdcLoggable {
// populateViewDefinitionCanSeeTransactionStatus()
alterCounterpartyLimitFieldType()
populateMigrationOfViewPermissions(startedBeforeSchemifier)
+ changeTypeOfAudFieldAtConsumerTable()
}
private def dummyScript(): Boolean = {
@@ -254,6 +255,12 @@ object Migration extends MdcLoggable {
MigrationOfConsumer.populateAzpAndSub(name)
}
}
+ private def changeTypeOfAudFieldAtConsumerTable(): Boolean = {
+ val name = nameOf(changeTypeOfAudFieldAtConsumerTable)
+ runOnce(name) {
+ MigrationOfConsumer.alterTypeofAud(name)
+ }
+ }
private def alterTableMappedUserAuthContext(startedBeforeSchemifier: Boolean): Boolean = {
if(startedBeforeSchemifier == true) {
logger.warn(s"Migration.database.alterTableMappedUserAuthContext(true) cannot be run before Schemifier.")
diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfConsumer.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfConsumer.scala
index 47e7e2ff6..1bd25bd2f 100644
--- a/obp-api/src/main/scala/code/api/util/migration/MigrationOfConsumer.scala
+++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfConsumer.scala
@@ -2,11 +2,11 @@ package code.api.util.migration
import java.time.format.DateTimeFormatter
import java.time.{ZoneId, ZonedDateTime}
-
import code.api.util.APIUtil
import code.api.util.migration.Migration.{DbFunction, saveLog}
import code.model.{AppType, Consumer}
-import net.liftweb.mapper.DB
+import net.liftweb.common.Full
+import net.liftweb.mapper.{DB, Schemifier}
import net.liftweb.util.{DefaultConnectionIdentifier, Helpers}
object MigrationOfConsumer {
@@ -107,4 +107,52 @@ object MigrationOfConsumer {
isSuccessful
}
}
+
+
+ def alterTypeofAud(name: String): Boolean = {
+ DbFunction.tableExists(Consumer) match {
+ case true =>
+ val startDate = System.currentTimeMillis()
+ val commitId: String = APIUtil.gitCommit
+ var isSuccessful = false
+
+ val executedSql =
+ DbFunction.maybeWrite(true, Schemifier.infoF _) {
+ APIUtil.getPropsValue("db.driver") match {
+ case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") =>
+ () =>
+ """
+ |ALTER TABLE consumer ALTER COLUMN aud VARCHAR(MAX) NULL;
+ |""".stripMargin
+ case _ =>
+ () =>
+ """
+ |ALTER TABLE consumer ALTER COLUMN aud TYPE text;
+ |""".stripMargin
+ }
+
+ }
+
+ val endDate = System.currentTimeMillis()
+ val comment: String =
+ s"""Executed SQL:
+ |$executedSql
+ |""".stripMargin
+ isSuccessful = true
+ saveLog(name, commitId, isSuccessful, startDate, endDate, comment)
+ isSuccessful
+
+ case false =>
+ val startDate = System.currentTimeMillis()
+ val commitId: String = APIUtil.gitCommit
+ val isSuccessful = false
+ val endDate = System.currentTimeMillis()
+ val comment: String =
+ s"""${Consumer._dbTableNameLC} table does not exist""".stripMargin
+ saveLog(name, commitId, isSuccessful, startDate, endDate, comment)
+ isSuccessful
+ }
+ }
+
+
}
diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala
index d31bfcb6e..db729eb4a 100644
--- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala
+++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala
@@ -12,6 +12,7 @@ import code.api.dynamic.entity.helper.DynamicEntityInfo
import code.api.util.APIUtil.{fullBoxOrException, _}
import code.api.util.ApiRole._
import code.api.util.ApiTag._
+import code.api.util.CommonsEmailWrapper._
import code.api.util.DynamicUtil.Validation
import code.api.util.ErrorMessages.{BankNotFound, _}
import code.api.util.ExampleValue._
@@ -79,8 +80,7 @@ import net.liftweb.json.JsonAST.JValue
import net.liftweb.json.JsonDSL._
import net.liftweb.json._
import net.liftweb.util.Helpers.{now, tryo}
-import net.liftweb.util.Mailer.{From, PlainMailBodyType, Subject, To, XHTMLMailBodyType}
-import net.liftweb.util.{Helpers, Mailer, StringHelpers}
+import net.liftweb.util.{Helpers, StringHelpers}
import org.apache.commons.lang3.StringUtils
import java.net.URLEncoder
@@ -91,7 +91,6 @@ import scala.collection.immutable.{List, Nil}
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.Future
import scala.jdk.CollectionConverters.collectionAsScalaIterableConverter
-import scala.xml.XML
trait APIMethods400 extends MdcLoggable {
self: RestHelper =>
@@ -3363,7 +3362,21 @@ trait APIMethods400 extends MdcLoggable {
.replace(WebUIPlaceholder.activateYourAccount, link)
logger.debug(s"customHtmlText: ${customHtmlText}")
logger.debug(s"Before send user invitation by email. Purpose: ${UserInvitationPurpose.DEVELOPER}")
- Mailer.sendMail(From(from), Subject(subject), To(invitation.email), PlainMailBodyType(customText), XHTMLMailBodyType(XML.loadString(customHtmlText)))
+
+ // Use Apache Commons Email wrapper instead of Lift Mailer
+ val emailContent = EmailContent(
+ from = from,
+ to = List(invitation.email),
+ subject = subject,
+ textContent = Some(customText),
+ htmlContent = Some(customHtmlText)
+ )
+
+ sendHtmlEmail(emailContent) match {
+ case Full(messageId) => logger.debug(s"Email sent successfully with Message-ID: $messageId")
+ case Empty => logger.error("Failed to send user invitation email")
+ }
+
logger.debug(s"After send user invitation by email. Purpose: ${UserInvitationPurpose.DEVELOPER}")
} else {
val subject = getWebUiPropsValue("webui_customer_user_invitation_email_subject", "Welcome to the API Playground")
@@ -3375,7 +3388,21 @@ trait APIMethods400 extends MdcLoggable {
.replace(WebUIPlaceholder.activateYourAccount, link)
logger.debug(s"customHtmlText: ${customHtmlText}")
logger.debug(s"Before send user invitation by email.")
- Mailer.sendMail(From(from), Subject(subject), To(invitation.email), PlainMailBodyType(customText), XHTMLMailBodyType(XML.loadString(customHtmlText)))
+
+ // Use Apache Commons Email wrapper instead of Lift Mailer
+ val emailContent = EmailContent(
+ from = from,
+ to = List(invitation.email),
+ subject = subject,
+ textContent = Some(customText),
+ htmlContent = Some(customHtmlText)
+ )
+
+ sendHtmlEmail(emailContent) match {
+ case Full(messageId) => logger.debug(s"Email sent successfully with Message-ID: $messageId")
+ case Empty => logger.error("Failed to send user invitation email")
+ }
+
logger.debug(s"After send user invitation by email.")
}
(JSONFactory400.createUserInvitationJson(invitation), HttpCode.`201`(callContext))
diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala
index a404c8220..711ad6643 100644
--- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala
+++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala
@@ -1708,6 +1708,10 @@ trait APIMethods510 {
|
|7 bank_id (ignore if omitted)
|
+ |8 provider_provider_id (ignore if omitted)
+ |provider and provider_id values are separated by pipe char
+ |eg: provider_provider_id=http%3A%2F%2Flocalhost%3A7070%2Frealms%2Fmaster|7837ee9c-3446-4d8c-9b90-301a52b4851d
+ |
|eg:/management/consents?consumer_id=78&limit=10&offset=10
|
""".stripMargin,
diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala
index 434047dc3..b13618de7 100644
--- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala
+++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala
@@ -46,12 +46,13 @@ import code.consent.MappedConsent
import code.metrics.APIMetric
import code.model.Consumer
import code.users.{UserAttribute, Users}
+import code.util.Helper.MdcLoggable
import code.views.system.{AccountAccess, ViewDefinition, ViewPermission}
import com.openbankproject.commons.model._
import com.openbankproject.commons.util.ApiVersion
import net.liftweb.common.{Box, Full}
import net.liftweb.json
-import net.liftweb.json.{JString, JValue, parse, parseOpt}
+import net.liftweb.json.{JString, JValue, MappingException, parse, parseOpt}
import java.text.SimpleDateFormat
import java.util.Date
@@ -705,7 +706,7 @@ case class ViewPermissionJson(
extra_data: Option[List[String]]
)
-object JSONFactory510 extends CustomJsonFormats {
+object JSONFactory510 extends CustomJsonFormats with MdcLoggable {
def createTransactionRequestJson(tr : TransactionRequest, transactionRequestAttributes: List[TransactionRequestAttributeTrait] ) : TransactionRequestJsonV510 = {
TransactionRequestJsonV510(
@@ -1009,7 +1010,16 @@ object JSONFactory510 extends CustomJsonFormats {
def createConsentsJsonV510(consents: List[MappedConsent]): ConsentsJsonV510 = {
ConsentsJsonV510(
consents.map { c =>
- val jwtPayload = JwtUtil.getSignedPayloadAsJson(c.jsonWebToken).map(parse(_).extract[ConsentJWT]).toOption
+ val jwtPayload = JwtUtil
+ .getSignedPayloadAsJson(c.jsonWebToken)
+ .flatMap { payload =>
+ Try(parse(payload).extract[ConsentJWT]).recover {
+ case e: MappingException =>
+ logger.warn(s"Invalid JWT payload: ${e.getMessage}")
+ null
+ }.toOption
+ }.toOption
+
AllConsentJsonV510(
consent_reference_id = c.consentReferenceId,
consumer_id = c.consumerId,
diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
index 8ac8f43f1..4446d8b78 100644
--- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
@@ -79,8 +79,7 @@ import net.liftweb.json
import net.liftweb.json.{JArray, JBool, JObject, JValue}
import net.liftweb.mapper._
import net.liftweb.util.Helpers.{hours, now, time, tryo}
-import net.liftweb.util.Mailer.{From, PlainMailBodyType, Subject, To}
-import net.liftweb.util.{Helpers, Mailer}
+import net.liftweb.util.Helpers
import org.mindrot.jbcrypt.BCrypt
import scalikejdbc.DB.CPContext
import scalikejdbc.{ConnectionPool, ConnectionPoolSettings, MultipleConnectionPoolContext, DB => scalikeDB, _}
@@ -383,8 +382,13 @@ object LocalMappedConnector extends Connector with MdcLoggable {
val hashedPassword = createHashedPassword(challengeAnswer)
APIUtil.getEmailsByUserId(userId) map {
pair =>
- val params = PlainMailBodyType(s"Your OTP challenge : ${challengeAnswer}") :: List(To(pair._2))
- Mailer.sendMail(From("challenge@tesobe.com"), Subject("Challenge"), params: _*)
+ val emailContent = CommonsEmailWrapper.EmailContent(
+ from = mailUsersUserinfoSenderAddress,
+ to = List(pair._2),
+ subject = "Challenge",
+ textContent = Some(s"Your OTP challenge : ${challengeAnswer}")
+ )
+ CommonsEmailWrapper.sendTextEmail(emailContent)
}
hashedPassword
case Some(StrongCustomerAuthentication.SMS) | Some(StrongCustomerAuthentication.SMS_OTP) =>
@@ -5185,12 +5189,13 @@ object LocalMappedConnector extends Connector with MdcLoggable {
_ <- Future{
scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL.toString => // Send the email
- val params = PlainMailBodyType(userAuthContextUpdate.challenge) :: List(To(customer.email))
- Mailer.sendMail(
- From("challenge@tesobe.com"),
- Subject("Challenge request"),
- params :_*
+ val emailContent = CommonsEmailWrapper.EmailContent(
+ from = mailUsersUserinfoSenderAddress,
+ to = List(customer.email),
+ subject = "Challenge request",
+ textContent = Some(userAuthContextUpdate.challenge)
)
+ CommonsEmailWrapper.sendTextEmail(emailContent)
case v if v == StrongCustomerAuthentication.SMS.toString => // Not implemented
case _ => // Not handled
}
@@ -5211,8 +5216,13 @@ object LocalMappedConnector extends Connector with MdcLoggable {
callContext: Option[CallContext]
): OBPReturnType[Box[String]] = {
if (scaMethod == StrongCustomerAuthentication.EMAIL){ // Send the email
- val params = PlainMailBodyType(message) :: List(To(recipient))
- Mailer.sendMail(From("challenge@tesobe.com"), Subject("OBP Consent Challenge"), params :_*)
+ val emailContent = CommonsEmailWrapper.EmailContent(
+ from = mailUsersUserinfoSenderAddress,
+ to = List(recipient),
+ subject = "OBP Consent Challenge",
+ textContent = Some(message)
+ )
+ CommonsEmailWrapper.sendTextEmail(emailContent)
Future{(Full("Success"), callContext)}
} else if (scaMethod == StrongCustomerAuthentication.SMS){ // Send the SMS
for {
diff --git a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnectionPool.scala b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnectionPool.scala
index 501264a95..5e52d9227 100644
--- a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnectionPool.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnectionPool.scala
@@ -2,14 +2,11 @@ package code.bankconnectors.rabbitmq
-import code.api.util.APIUtil
-import com.rabbitmq.client.{Connection, ConnectionFactory}
-import org.apache.commons.pool2.impl.{GenericObjectPool, GenericObjectPoolConfig}
-import org.apache.commons.pool2.BasePooledObjectFactory
-import org.apache.commons.pool2.PooledObject
-import org.apache.commons.pool2.impl.DefaultPooledObject
+import code.api.util.{APIUtil, ErrorMessages}
import code.bankconnectors.rabbitmq.RabbitMQUtils._
-import code.api.util.ErrorMessages
+import com.rabbitmq.client.{Connection, ConnectionFactory}
+import org.apache.commons.pool2.{BasePooledObjectFactory, PooledObject}
+import org.apache.commons.pool2.impl.{DefaultPooledObject, GenericObjectPool, GenericObjectPoolConfig}
class RabbitMQConnectionFactory extends BasePooledObjectFactory[Connection] {
@@ -59,7 +56,7 @@ class RabbitMQConnectionFactory extends BasePooledObjectFactory[Connection] {
// Pool to manage RabbitMQ connections
object RabbitMQConnectionPool {
- private val poolConfig = new GenericObjectPoolConfig()
+ private val poolConfig = new GenericObjectPoolConfig[Connection]()
poolConfig.setMaxTotal(5) // Maximum number of connections
poolConfig.setMinIdle(2) // Minimum number of idle connections
poolConfig.setMaxIdle(5) // Maximum number of idle connections
diff --git a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala
index d03236dd0..c644945b7 100644
--- a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala
@@ -1552,7 +1552,7 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable {
startDate=toDate(transactionStartDateExample),
finishDate=Some(toDate(transactionFinishDateExample)),
balance=BigDecimal(balanceExample.value),
- status=transactionStatusExample.value
+ status=Some(transactionStatusExample.value)
)))
),
adapterImplementation = Some(AdapterImplementation("- Core", 1))
@@ -1687,7 +1687,7 @@ trait RabbitMQConnector_vOct2024 extends Connector with MdcLoggable {
startDate=toDate(transactionStartDateExample),
finishDate=Some(toDate(transactionFinishDateExample)),
balance=BigDecimal(balanceExample.value),
- status=transactionStatusExample.value))
+ status=Some(transactionStatusExample.value)))
),
adapterImplementation = Some(AdapterImplementation("- Core", 1))
)
diff --git a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala
index 6bfee72bd..53a3b7200 100644
--- a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala
@@ -1500,7 +1500,7 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable {
startDate=toDate(transactionStartDateExample),
finishDate=Some(toDate(transactionFinishDateExample)),
balance=BigDecimal(balanceExample.value),
- status=transactionStatusExample.value)))
+ status=Some(transactionStatusExample.value))))
),
adapterImplementation = Some(AdapterImplementation("- Core", 1))
)
@@ -1634,7 +1634,7 @@ trait RestConnector_vMar2019 extends Connector with MdcLoggable {
startDate=toDate(transactionStartDateExample),
finishDate=Some(toDate(transactionFinishDateExample)),
balance=BigDecimal(balanceExample.value),
- status=transactionStatusExample.value))
+ status=Some(transactionStatusExample.value)))
),
adapterImplementation = Some(AdapterImplementation("- Core", 1))
)
diff --git a/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala b/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala
index 0ab7b583b..d3a89839a 100644
--- a/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala
@@ -1481,7 +1481,7 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable {
startDate=toDate(transactionStartDateExample),
finishDate=Some(toDate(transactionFinishDateExample)),
balance=BigDecimal(balanceExample.value),
- status=transactionStatusExample.value)))
+ status=Some(transactionStatusExample.value))))
),
adapterImplementation = Some(AdapterImplementation("- Core", 1))
)
@@ -1615,7 +1615,7 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable {
startDate=toDate(transactionStartDateExample),
finishDate=Some(toDate(transactionFinishDateExample)),
balance=BigDecimal(balanceExample.value),
- status=transactionStatusExample.value))
+ status=Some(transactionStatusExample.value)))
),
adapterImplementation = Some(AdapterImplementation("- Core", 1))
)
diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala
index 21f5bca8a..6fa453465 100644
--- a/obp-api/src/main/scala/code/consent/MappedConsent.scala
+++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala
@@ -1,17 +1,20 @@
package code.consent
import java.util.Date
-import code.api.util.{APIUtil, Consent, ErrorMessages, OBPBankId, OBPConsentId, OBPConsumerId, OBPLimit, OBPOffset, OBPQueryParam, OBPSortBy, OBPStatus, OBPUserId, SecureRandomUtil}
+import code.api.util.{APIUtil, Consent, ErrorMessages, OBPBankId, OBPConsentId, OBPConsumerId, OBPLimit, OBPOffset, OBPQueryParam, OBPSortBy, OBPStatus, OBPUserId, ProviderProviderId, SecureRandomUtil}
import code.consent.ConsentStatus.ConsentStatus
import code.model.Consumer
+import code.model.dataAccess.ResourceUser
import code.util.MappedUUID
import com.openbankproject.commons.model.User
import com.openbankproject.commons.util.ApiStandards
import net.liftweb.common.{Box, Empty, Failure, Full}
-import net.liftweb.mapper.{MappedString, _}
+import net.liftweb.mapper._
import net.liftweb.util.Helpers.{now, tryo}
import org.mindrot.jbcrypt.BCrypt
+import java.net.URLDecoder
+import java.nio.charset.StandardCharsets
import scala.collection.immutable.List
object MappedConsentProvider extends ConsentProvider {
@@ -71,6 +74,20 @@ object MappedConsentProvider extends ConsentProvider {
// The optional variables:
val consumerId = queryParams.collectFirst { case OBPConsumerId(value) => By(MappedConsent.mConsumerId, value) }
val consentId = queryParams.collectFirst { case OBPConsentId(value) => By(MappedConsent.mConsentId, value) }
+ val providerProviderId: Option[Cmp[MappedConsent, String]] = queryParams.collectFirst {
+ case ProviderProviderId(value) =>
+ val (provider, providerId) = value.split("\\|") match { // split by literal '|'
+ case Array(a, b) => (a, b)
+ case _ => ("", "") // fallback if format is unexpected
+ }
+ ResourceUser.findAll(By(ResourceUser.provider_, provider), By(ResourceUser.providerId, providerId)) match {
+ case x :: Nil => // exactly one
+ Some(By(MappedConsent.mUserId, x.userId))
+ case _ =>
+ None
+ }
+ }.flatten
+
val userId = queryParams.collectFirst { case OBPUserId(value) => By(MappedConsent.mUserId, value) }
val status = queryParams.collectFirst {
case OBPStatus(value) =>
@@ -96,7 +113,7 @@ object MappedConsentProvider extends ConsentProvider {
offset.toSeq,
limit.toSeq,
status.toSeq,
- userId.toSeq,
+ userId.orElse(providerProviderId).toSeq,
consentId.toSeq,
consumerId.toSeq
).flatten
diff --git a/obp-api/src/main/scala/code/model/ModeratedBankingData.scala b/obp-api/src/main/scala/code/model/ModeratedBankingData.scala
index 7314db295..569b48f99 100644
--- a/obp-api/src/main/scala/code/model/ModeratedBankingData.scala
+++ b/obp-api/src/main/scala/code/model/ModeratedBankingData.scala
@@ -57,7 +57,7 @@ class ModeratedTransaction(
//the filteredBlance type in this class is a string rather than Big decimal like in Transaction trait for snippet (display) reasons.
//the view should be able to return a sign (- or +) or the real value. casting signs into big decimal is not possible
val balance : String,
- val status : String
+ val status : Moderated[String]
) {
def dateOption2JString(date: Option[Date]) : JString = {
diff --git a/obp-api/src/main/scala/code/model/OAuth.scala b/obp-api/src/main/scala/code/model/OAuth.scala
index bf23e5719..f5b1b8c65 100644
--- a/obp-api/src/main/scala/code/model/OAuth.scala
+++ b/obp-api/src/main/scala/code/model/OAuth.scala
@@ -541,7 +541,7 @@ class Consumer extends LongKeyedMapper[Consumer] with CreatedUpdated{
// because different databases treat unique indexes on NULL values differently.
override def defaultValue = APIUtil.generateUUID()
}
- object aud extends MappedString(this, 250) {
+ object aud extends MappedText(this) {
override def defaultValue = null
}
object iss extends MappedString(this, 250) {
diff --git a/obp-api/src/main/scala/code/model/View.scala b/obp-api/src/main/scala/code/model/View.scala
index 8f653f891..1ced4ecf8 100644
--- a/obp-api/src/main/scala/code/model/View.scala
+++ b/obp-api/src/main/scala/code/model/View.scala
@@ -178,7 +178,7 @@ case class ViewExtended(val view: View) {
val transactionStatus =
if (viewPermissions.exists(_ == CAN_SEE_TRANSACTION_STATUS)) transaction.status
- else ""
+ else None
new ModeratedTransaction(
UUID = transactionUUID,
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 29db243af..0d9334ff5 100644
--- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala
+++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala
@@ -33,6 +33,7 @@ import code.api.cache.Caching
import code.api.dynamic.endpoint.helper.DynamicEndpointHelper
import code.api.util.APIUtil._
import code.api.util.CommonFunctions.validUri
+import code.api.util.CommonsEmailWrapper._
import code.api.util.ErrorMessages._
import code.api.util._
import code.bankconnectors.Connector
@@ -55,7 +56,6 @@ import net.liftweb.http.S.fmapFunc
import net.liftweb.http._
import net.liftweb.mapper._
import net.liftweb.sitemap.Loc.{If, LocParam, Template}
-import net.liftweb.util.Mailer.{BCC, From, Subject, To}
import net.liftweb.util._
import org.apache.commons.lang3.StringUtils
import sh.ory.hydra.api.AdminApi
@@ -589,24 +589,35 @@ 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())
- Mailer.sendMail(From(emailFrom),Subject(passwordResetEmailSubject + " - " + u.username),
- To(u.getEmail) ::
- generateResetEmailBodies(u, resetPasswordLink) :::
- (bccEmail.toList.map(BCC(_))) :_*)
+ // 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),
+ bcc = bccEmail.toList,
+ subject = passwordResetEmailSubject + " - " + u.username,
+ textContent = textContent,
+ htmlContent = htmlContent
+ )
+ sendHtmlEmail(emailContent) match {
+ case Full(messageId) =>
+ logger.debug(s"Password reset email sent successfully with Message-ID: $messageId")
+ S.notice("Password reset email sent successfully. Please check your email.")
+ S.redirectTo(homePage)
+ case Empty =>
+ logger.error("Failed to send password reset email")
+ S.error("Failed to send password reset email. Please try again.")
+ S.redirectTo(homePage)
+ }
case u =>
sendValidationEmail(u)
}
- // In order to prevent any leakage of information we use the same message for all cases
- S.notice(userNameNotFoundString)
- S.redirectTo(homePage)
}
override def lostPasswordXhtml = {
@@ -638,17 +649,26 @@ 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)
-
- Mailer.sendMail(From(emailFrom),Subject(signupMailSubject),
- To(user.getEmail) ::
- generateValidationEmailBodies(user, resetLink) :::
- (bccEmail.toList.map(BCC(_))) :_* )
+ 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),
+ bcc = bccEmail.toList,
+ subject = signupMailSubject,
+ textContent = textContent,
+ htmlContent = htmlContent
+ )
+ sendHtmlEmail(emailContent) match {
+ case Full(messageId) =>
+ logger.debug(s"Validation email sent successfully with Message-ID: $messageId")
+ S.notice("Validation email sent successfully. Please check your email.")
+ case Empty =>
+ logger.error("Failed to send validation email")
+ S.error("Failed to send validation email. Please try again.")
+ }
}
def grantDefaultEntitlementsToAuthUser(user: TheUserType) = {
diff --git a/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala b/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala
index 0e25cf923..b79da7d55 100644
--- a/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala
+++ b/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala
@@ -28,7 +28,7 @@ package code.snippet
import java.util
import code.api.{Constant, DirectLogin}
-import code.api.util.{APIUtil, ErrorMessages, KeycloakAdmin, X509}
+import code.api.util.{APIUtil, ErrorMessages, KeycloakAdmin, X509, CommonsEmailWrapper}
import code.consumer.Consumers
import code.model.dataAccess.AuthUser
import code.model.{Consumer, _}
@@ -424,18 +424,19 @@ class ConsumerRegistration extends MdcLoggable {
s"Direct Login Documentation: ${oauthDocumentationUrl} \n" +
s"$registrationMoreInfoText: $registrationMoreInfoUrl"
- val params = PlainMailBodyType(registrationMessage) :: List(To(registered.developerEmail.get))
-
val webuiRegisterConsumerSuccessMssageEmail : String = getWebUiPropsValue(
"webui_register_consumer_success_message_email",
"Thank you for registering to use the Open Bank Project API.")
- //this is an async call
- Mailer.sendMail(
- From(from),
- Subject(webuiRegisterConsumerSuccessMssageEmail),
- params :_*
+ val emailContent = CommonsEmailWrapper.EmailContent(
+ from = from,
+ to = List(registered.developerEmail.get),
+ subject = webuiRegisterConsumerSuccessMssageEmail,
+ textContent = Some(registrationMessage)
)
+
+ //this is an async call
+ CommonsEmailWrapper.sendTextEmail(emailContent)
}
if(mailSent.isEmpty)
@@ -445,8 +446,6 @@ class ConsumerRegistration extends MdcLoggable {
// This is to let the system administrators / API managers know that someone has registered a consumer key.
def notifyRegistrationOccurred(registered : Consumer) = {
- import net.liftweb.util.Mailer
- import net.liftweb.util.Mailer._
val mailSent = for {
// e.g mail.api.consumer.registered.sender.address=no-reply@example.com
@@ -465,18 +464,18 @@ class ConsumerRegistration extends MdcLoggable {
//technically doesn't work for all valid email addresses so this will mess up if someone tries to send emails to "foo,bar"@example.com
val to = toAddressesString.split(",").toList
- val toParams = to.map(To(_))
- val params = PlainMailBodyType(registrationMessage) :: toParams
+
+ val emailContent = CommonsEmailWrapper.EmailContent(
+ from = from,
+ to = to,
+ subject = s"New API user registered on $thisApiInstance",
+ textContent = Some(registrationMessage)
+ )
//this is an async call
- Mailer.sendMail(
- From(from),
- Subject(s"New API user registered on $thisApiInstance"),
- params :_*
- )
+ CommonsEmailWrapper.sendTextEmail(emailContent)
}
- //if Mailer.sendMail wasn't called (note: this actually isn't checking if the mail failed to send as that is being done asynchronously)
if(mailSent.isEmpty)
this.logger.warn(s"API consumer registration failed: $mailSent")
diff --git a/obp-api/src/main/scala/code/transaction/MappedTransaction.scala b/obp-api/src/main/scala/code/transaction/MappedTransaction.scala
index d17879a24..378f74dd7 100644
--- a/obp-api/src/main/scala/code/transaction/MappedTransaction.scala
+++ b/obp-api/src/main/scala/code/transaction/MappedTransaction.scala
@@ -156,7 +156,7 @@ class MappedTransaction extends LongKeyedMapper[MappedTransaction] with IdPK wit
tStartDate.get,
Some(tFinishDate.get),
newBalance,
- status.get))
+ Some(status.get)))
}
}
diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3Test.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3Test.scala
index 8e0cda195..7b77900ba 100644
--- a/obp-api/src/test/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3Test.scala
+++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3Test.scala
@@ -91,7 +91,7 @@ class JSONFactory_BERLIN_GROUP_1_3Test extends FeatureSpec with Matchers with Gi
startDate = Some(new java.util.Date()),
finishDate = Some(new java.util.Date()),
balance = "900.00",
- status = "booked"
+ status = Some("booked")
)
}
diff --git a/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_frozen_meta_data b/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_frozen_meta_data
index a802ceacd..ffdf640c9 100644
Binary files a/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_frozen_meta_data and b/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_frozen_meta_data differ
diff --git a/obp-commons/pom.xml b/obp-commons/pom.xml
index c7f68bad4..b41909faf 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
diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala
index 5a300bf6a..c81aac363 100644
--- a/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala
+++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala
@@ -1141,7 +1141,7 @@ case class Transaction(
finishDate : Option[Date],
//the new balance for the bank account
balance : BigDecimal,
- status: String
+ status : Option[String]
) {
val bankId = thisAccount.bankId
diff --git a/release_notes.md b/release_notes.md
index 373143b9b..03a17f3ed 100644
--- a/release_notes.md
+++ b/release_notes.md
@@ -3,6 +3,20 @@
### Most recent changes at top of file
```
Date Commit Action
+04/08/2025 d282d266 Enhanced Email Configuration with CommonsEmailWrapper
+ Replaced Lift Mailer with Apache Commons Email for improved email functionality.
+ Added comprehensive SMTP configuration options:
+ - mail.smtp.auth (authentication support)
+ - mail.smtp.user (SMTP username)
+ - mail.smtp.password (SMTP password)
+ - mail.smtp.starttls.enable (STARTTLS support)
+ - mail.smtp.ssl.enable (SSL support)
+ - mail.smtp.ssl.protocols (TLS protocol selection)
+ - mail.smtp.ssl.trust (certificate trust options)
+ - mail.debug (SMTP debugging)
+ - mail.users.userinfo.sender.address (default sender)
+ Added configuration examples forTesobe mail servers.
+ All email functionality (password reset, validation, notifications) now uses CommonsEmailWrapper.
25/06/2025 e49ebb4f Added props BG_remove_sign_of_amounts, default is false.
If set to true, the sign of amounts will be removed in the BGv1.3 getTransaction endpoints.
17/03/2025 166e4f2a Removed Kafka commits: 166e4f2a,7f24802e,6f0a3b53,f22763c3,