diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac1fc2861..66ddf7d33 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,11 +2,7 @@ name: Build, format & test workflow on: [push] env: - POSTGRESQL_ADDON_USER: signalconso - POSTGRESQL_ADDON_HOST: localhost - POSTGRESQL_ADDON_PORT: 5432 - POSTGRESQL_ADDON_DB: test_signalconso - POSTGRESQL_ADDON_PASSWORD : + USER: signalconso USE_TEXT_LOGS: true jobs: diff --git a/app/config/EmailConfiguration.scala b/app/config/EmailConfiguration.scala index acd3a84a4..b7a079587 100644 --- a/app/config/EmailConfiguration.scala +++ b/app/config/EmailConfiguration.scala @@ -9,5 +9,6 @@ case class EmailConfiguration( contactAddress: EmailAddress, skipReportEmailValidation: Boolean, emailProvidersBlocklist: List[String], - outboundEmailFilterRegex: Regex + outboundEmailFilterRegex: Regex, + maxRecipientsPerEmail: Int ) diff --git a/app/models/report/ReportDraft.scala b/app/models/report/ReportDraft.scala index fbc8872de..b0362d186 100644 --- a/app/models/report/ReportDraft.scala +++ b/app/models/report/ReportDraft.scala @@ -100,6 +100,8 @@ case class ReportDraft( if (employeeConsumer) ReportStatus.LanceurAlerte else if (!shouldBeVisibleToPro()) ReportStatus.NA else if (companySiret.isEmpty) ReportStatus.NA + else if (companySiret.nonEmpty && companyAddress.flatMap(_.country).isDefined) + ReportStatus.NA // Company has a french SIRET but a foreign address, we can't send any letter to it else ReportStatus.TraitementEnCours } diff --git a/app/services/MailService.scala b/app/services/MailService.scala index ffce4046b..9fa9f4347 100644 --- a/app/services/MailService.scala +++ b/app/services/MailService.scala @@ -82,17 +82,19 @@ class MailService( ): Future[Unit] = { val filteredEmptyEmail: Seq[EmailAddress] = filterEmail(recipients) NonEmptyList.fromList(filteredEmptyEmail.toList) match { - case None => + case None => () case Some(filteredRecipients) => - val emailRequest = EmailRequest( - from = mailFrom, - recipients = filteredRecipients, - subject = subject, - bodyHtml = bodyHtml, - attachments = attachments - ) - // we launch this but don't wait for its completion - mailRetriesService.sendEmailWithRetries(emailRequest) + filteredRecipients.grouped(emailConfiguration.maxRecipientsPerEmail).foreach { groupedRecipients => + val emailRequest = EmailRequest( + from = mailFrom, + recipients = groupedRecipients, + subject = subject, + bodyHtml = bodyHtml, + attachments = attachments + ) + // we launch this but don't wait for its completion + mailRetriesService.sendEmailWithRetries(emailRequest) + } } Future.unit } diff --git a/app/utils/Country.scala b/app/utils/Country.scala index 6ceccbc7f..65e2f20a1 100644 --- a/app/utils/Country.scala +++ b/app/utils/Country.scala @@ -179,7 +179,7 @@ object Country { val Seychelles = Country("SC", "Seychelles", "Seychelles") val SierraLeone = Country("SL", "Sierra Leone", "Sierra Leone") val Singapour = Country("SG", "Singapour", "Singapore") - val Slovaquie = Country("SL", "Slovaquie", "Slovakia", european = true) + val Slovaquie = Country("SK", "Slovaquie", "Slovakia", european = true) val Slovenie = Country("SI", "Slovénie", "Slovenia", european = true) val Somalie = Country("SO", "Somalie", "Somalia") val Soudan = Country("SD", "Soudan", "Sudan") @@ -205,12 +205,54 @@ object Country { val Ukraine = Country("UA", "Ukraine", "Ukraine") val Uruguay = Country("UY", "Uruguay", "Uruguay") val Vanuatu = Country("VU", "Vanuatu", "Vanuatu") - val Vatican = Country("VAT", "Vatican", "Vatican City") + val Vatican = Country("VA", "Vatican", "Vatican City") val Venezuela = Country("VE", "Vénézuéla", "Venezuela") val Vietnam = Country("VN", "Vietnam", "Vietnam") val Yemen = Country("YE", "Yémen", "Yemen") val Zambie = Country("ZM", "Zambie", "Zambia") val Zimbabwe = Country("ZW", "Zimbabwé", "Zimbabwe") + val IlesFeroe = Country("FO", "Îles Féroé", "Faroe Islands") + val Svalbard = Country("SJ", "Svalbard et Île Jan Mayen", "Svalbard and Jan Mayen") + val IleBouvet = Country("BV", "Île Bouvet", "Bouvet Island") + val Jersey = Country("JE", "Jersey", "Jersey") + val IleMan = Country("IM", "Île Man", "Isle of Man") + val Guernsey = Country("GG", "Guernsey", "Bailiwick of Guernsey") + val Gibraltar = Country("GI", "Gibraltar", "Gibraltar") + val Aruba = Country("AW", "Aruba", "Aruba") + val HongKong = Country("HK", "Hong-Kong", "Hong Kong") + val Macao = Country("MO", "Macao", "Macau") + val Taiwan = Country("TW", "Taiwan", "Taiwan") + val Palestine = Country("PS", "État de Palestine", "State of Palestine") + val SainteHelene = + Country("SH", "Sainte-Hélène, Ascension et Tristan da Cunha", "Saint Helena, Ascension and Tristan da Cunha") + val TerritoireBritannique = + Country("IO", "Territoire britannique de l'océan Indien", "British Indian Ocean Territory") + val SaharaOccidental = Country("EH", "Sahara occidental", "Western Sahara") + val IlesViergesBritanniques = Country("VG", "Îles Vierges britanniques", "British Virgin Islands") + val IlesTurques = Country("TC", "Îles Turques-et-Caïques", "Turks and Caicos Islands") + val Montserrat = Country("MS", "Montserrat", "Montserrat") + val IlesCaimans = Country("KY", "Îles Caïmans", "Cayman Islands") + val Bermudes = Country("BM", "Bermudes", "Bermuda") + val Anguilla = Country("AI", "Anguilla", "Anguilla") + val GeorgieDuSud = + Country("GS", "Géorgie du Sud-et-les îles Sandwich du Sud", "South Georgia and the South Sandwich Islands") + val Falkland = Country("FK", "Îles Malouines ou Falkland", "Falkland Islands") + val Groenland = Country("GL", "Groenland", "Greenland") + val AntillesNeerlandaises = Country("AN", "Antilles néerlandaises", "Netherlands Antilles") + val IlesVierges = Country("VI", "Îles Vierges des États-Unis", "United States Virgin Islands") + val PortoRico = Country("PR", "Porto Rico", "Puerto Rico") + val Bonaire = Country("BQ", "Bonaire, Saint Eustache et Saba", "Bonaire, Sint Eustatius and Saba") + val Curacao = Country("CW", "Curaçao", "Curaçao") + val SaintMartinNL = Country("SX", "Saint-Martin (royaume des Pays-Bas)", "Sint Maarten (Dutch part)") + val Norfolk = Country("NF", "Île Norfolk", "Norfolk Island") + val MacDo = Country("HM", "Îles Heard-et-MacDonald", "Heard Island and McDonald Islands") + val Christmas = Country("CX", "Île Christmas", "Christmas Island") + val IlesCocos = Country("CC", "Îles Cocos", "Cocos (Keeling) Islands") + val Tokelau = Country("TK", "Tokelau", "Tokelau") + val Pitcairn = Country("PN", "Îles Pitcairn", "Pitcairn Islands") + val IlesMariannes = Country("MP", "Îles Mariannes du Nord", "Northern Mariana Islands") + val Guam = Country("GU", "Guam", "Guam") + val SamoaAmericaines = Country("AS", "Samoa américaines", "American Samoa") val countries = List( Afghanistan, @@ -409,7 +451,46 @@ object Country { Vietnam, Yemen, Zambie, - Zimbabwe + Zimbabwe, + IlesFeroe, + Svalbard, + IleBouvet, + Jersey, + IleMan, + Guernsey, + Gibraltar, + Aruba, + HongKong, + Macao, + Taiwan, + Palestine, + SainteHelene, + TerritoireBritannique, + SaharaOccidental, + IlesViergesBritanniques, + IlesTurques, + Montserrat, + IlesCaimans, + Bermudes, + Anguilla, + GeorgieDuSud, + Falkland, + Groenland, + AntillesNeerlandaises, + IlesVierges, + PortoRico, + Bonaire, + Curacao, + SaintMartinNL, + Norfolk, + MacDo, + Christmas, + IlesCocos, + Tokelau, + Pitcairn, + IlesMariannes, + Guam, + SamoaAmericaines ) def fromCode(code: String) = @@ -419,7 +500,7 @@ object Country { countries.find(_.name == name).head implicit val reads = new Reads[Country] { - def reads(json: JsValue): JsResult[Country] = json.validate[String].map(fromName(_)) + def reads(json: JsValue): JsResult[Country] = json.validate[String].map(fromCode) } implicit val writes = Json.writes[Country] diff --git a/conf/common/database.conf b/conf/common/database.conf index 74dfe69e9..7c8a2c04b 100644 --- a/conf/common/database.conf +++ b/conf/common/database.conf @@ -10,11 +10,11 @@ slick.dbs.default.db { // Used for flyway flyway { - host = ${POSTGRESQL_ADDON_HOST} - port = ${POSTGRESQL_ADDON_PORT} - database = ${POSTGRESQL_ADDON_DB}, - user = ${POSTGRESQL_ADDON_USER} - password = ${POSTGRESQL_ADDON_PASSWORD} + host = ${?POSTGRESQL_ADDON_HOST} + port = ${?POSTGRESQL_ADDON_PORT} + database = ${?POSTGRESQL_ADDON_DB}, + user = ${?POSTGRESQL_ADDON_USER} + password = ${?POSTGRESQL_ADDON_PASSWORD} // DATA_LOSS / DESTRUCTIVE / BE AWARE ---- Keep to "false" //Be careful when enabling this as it removes the safety net that ensures Flyway does not migrate the wrong database in case of a configuration mistake! //This is useful for initial Flyway production deployments on projects with an existing DB. diff --git a/conf/common/mail.conf b/conf/common/mail.conf index 2434dc3dc..1923c054a 100644 --- a/conf/common/mail.conf +++ b/conf/common/mail.conf @@ -34,4 +34,7 @@ mail { # Filter outbound emails on test / dev / local env outbound-email-filter-regex = ".*" outbound-email-filter-regex = ${?OUTBOUND_EMAIL_FILTER_REGEX} + + // To prevent SMTPAddressFailedException 552 5.5.3 Maximum limit of 50 recipients reached + max-recipients-per-email = 40 } \ No newline at end of file diff --git a/conf/test.application.conf b/conf/test.application.conf index 2028b59f7..527b09c39 100644 --- a/conf/test.application.conf +++ b/conf/test.application.conf @@ -9,11 +9,24 @@ test.db { } -test.db.user = ${?POSTGRESQL_ADDON_USER} +test.db.user =${?USER} test.db.host = "localhost" test.db.port = 5432 test.db.name = "test_signalconso" +flyway { + host = "localhost" + port = 5432 + database = "test_signalconso" + user = ${?USER} + password =password + // DATA_LOSS / DESTRUCTIVE / BE AWARE ---- Keep to "false" + //Be careful when enabling this as it removes the safety net that ensures Flyway does not migrate the wrong database in case of a configuration mistake! + //This is useful for initial Flyway production deployments on projects with an existing DB. + //See https://flywaydb.org/documentation/configuration/parameters/baselineOnMigrate for more information + baseline-on-migrate = false +} + slick.dbs.default.db.properties.url = "postgres://"${test.db.user}"@"${test.db.host}":"${test.db.port}"/"${test.db.name} slick.dbs.default.db.connectionPool = "disabled" diff --git a/test/controllers/EmailValidationControllerSpec.scala b/test/controllers/EmailValidationControllerSpec.scala index ac6f8ea32..80e922d16 100644 --- a/test/controllers/EmailValidationControllerSpec.scala +++ b/test/controllers/EmailValidationControllerSpec.scala @@ -72,7 +72,8 @@ class EmailValidationControllerSpec(implicit ee: ExecutionEnv) contactAddress = EmptyEmailAddress, skipReportEmailValidation = skipValidation, emailProvidersBlocklist = emailProviderBlocklist, - outboundEmailFilterRegex = ".*".r + outboundEmailFilterRegex = ".*".r, + maxRecipientsPerEmail = 40 ) } diff --git a/test/controllers/ReportControllerSpec.scala b/test/controllers/ReportControllerSpec.scala index 6f238e25e..c955af598 100644 --- a/test/controllers/ReportControllerSpec.scala +++ b/test/controllers/ReportControllerSpec.scala @@ -249,7 +249,8 @@ class ReportControllerSpec(implicit ee: ExecutionEnv) extends Specification with EmailAddress("test@sc.com"), skipValidation, List(""), - ".*".r + ".*".r, + 40 ) } diff --git a/test/services/MailServiceSpec.scala b/test/services/MailServiceSpec.scala index 42ae24a29..7990416ae 100644 --- a/test/services/MailServiceSpec.scala +++ b/test/services/MailServiceSpec.scala @@ -304,6 +304,61 @@ class MailServiceSpecFilteredEmail(implicit ee: ExecutionEnv) extends BaseMailSe } } +class MailServiceSpecGroupForMaxRecipients(implicit ee: ExecutionEnv) extends BaseMailServiceSpec { + + override def is = s2"""email must be grouped to prevent exceeding recipients limit $e1""" + + def e1 = { + + val recipients = List( + EmailAddress(s"1@email.fr"), + EmailAddress(s"2@email.fr"), + EmailAddress(s"3@email.fr") + ) + + val mailService = new MailService( + mockMailRetriesService, + emailConfiguration = emailConfiguration.copy(maxRecipientsPerEmail = 2), + reportNotificationBlocklistRepo = components.reportNotificationBlockedRepository, + pdfService = components.pdfService, + attachmentService = components.attachmentService + )( + components.frontRoute, + executionContext + ) + + ProNewReportNotification( + NonEmptyList.of(EmailAddress(s"1@email.fr"), EmailAddress(s"2@email.fr")), + reportForSubsidiary + ) + + Await.result( + mailService.send( + ProNewReportNotification( + NonEmptyList.fromListUnsafe(recipients), + reportForSubsidiary + ) + ), + Duration.Inf + ) + + there was one(mockMailRetriesService).sendEmailWithRetries( + argThat((emailRequest: EmailRequest) => + emailRequest.recipients.size == 2 && emailRequest.recipients.toList.containsSlice( + List(EmailAddress(s"1@email.fr"), EmailAddress(s"2@email.fr")) + ) + ) + ) + + there was one(mockMailRetriesService).sendEmailWithRetries( + argThat((emailRequest: EmailRequest) => + emailRequest.recipients.size == 1 && emailRequest.recipients.head == EmailAddress(s"3@email.fr") + ) + ) + + } +} + //MailServiceSpecNoBlock //MailServiceSpecSomeBlock //MailServiceSpecAllBlock