diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 5dad12a56..feb7d2054 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -178,6 +178,11 @@ httpclient 4.5.13 + + org.apache.commons + commons-pool2 + 2.11.1 + org.eclipse.jetty jetty-util @@ -397,6 +402,12 @@ org.asynchttpclient async-http-client 2.10.4 + + + javax.activation + com.sun.activation + + @@ -405,6 +416,16 @@ org.scalikejdbc scalikejdbc_${scala.version} 3.4.0 + + + com.sun.activation + javax.activation + + + javax.activation + activation + + com.microsoft.sqlserver @@ -498,6 +519,16 @@ test + + com.sun.mail + jakarta.mail + 2.0.1 + + + jakarta.activation + jakarta.activation-api + 2.0.1 + 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,