diff --git a/integraatio/src/test/scala/fi/oph/viestinvalitys/IntegraatioTest.scala b/integraatio/src/test/scala/fi/oph/viestinvalitys/IntegraatioTest.scala index 260ed2cf..b075ad05 100644 --- a/integraatio/src/test/scala/fi/oph/viestinvalitys/IntegraatioTest.scala +++ b/integraatio/src/test/scala/fi/oph/viestinvalitys/IntegraatioTest.scala @@ -6,7 +6,8 @@ import com.fasterxml.jackson.datatype.jdk8.Jdk8Module import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.nimbusds.jose.util.StandardCharset -import fi.oph.viestinvalitys.business.{Kieli, Kontakti, Lahetys, Liite, LiitteenTila, Prioriteetti, SisallonTyyppi, VastaanottajanTila, Viesti} +import fi.oph.viestinvalitys.business.{Kieli, Kontakti, Lahetys, Liite, LiitteenTila, Prioriteetti, SisallonTyyppi, VastaanottajanSiirtyma, VastaanottajanTila, Viesti} +import fi.oph.viestinvalitys.lahetys.LambdaHandler.SAHKOPOSTIOSOITE_EI_VALIDI_ERROR import fi.oph.viestinvalitys.security.{AuditLog, AuditOperation} import fi.oph.viestinvalitys.util.AwsUtil import fi.oph.viestinvalitys.vastaanotto.model.Lahetys.Lahettaja @@ -767,4 +768,29 @@ class IntegraatioTest extends BaseIntegraatioTesti { }, 60.seconds) catch case e: Exception => Assertions.fail("Vastaanottaja ei muuttunut delivery-tilaan sallitussa ajassa") + + /** + * Testataan virheellisen vastaanottajan (sähköposti ei validi) lähetys. Pitää mennä virhetilaan. + */ + @Test def testLuoViestiVirheellinenVastaanottaja(): Unit = + // luodaan viesti + val luoViestiResult = mvc.perform(jsonPost(LahetysAPIConstants.LUO_VIESTI_PATH, getViesti(vastaanottajat = + java.util.List.of(VastaanottajaImpl(Optional.empty(), Optional.of("täysin väärä osoite"))))) + .`with`(user("kayttaja").roles(SecurityConstants.SECURITY_ROOLI_LAHETYS_FULL.replace("ROLE_", "")))) + .andExpect(status().isOk()).andReturn() + val response = objectMapper.readValue(luoViestiResult.getResponse.getContentAsString, classOf[LuoViestiSuccessResponseImpl]) + val vastaanottaja = kantaOperaatiot.getLahetyksenVastaanottajat(response.lahetysTunniste, Option.empty, Option.empty).head + + // haetaan viestin ainoan vastaanottajan tila sekunnin välein kunnes tilassa VIRHE + var siirtyma: VastaanottajanSiirtyma = null + try + Await.ready(Future { + while (siirtyma==null || !VastaanottajanTila.VIRHE.equals(siirtyma.tila)) + Thread.sleep(1000) + siirtyma = kantaOperaatiot.getVastaanottajanSiirtymat(vastaanottaja.tunniste).head + }, 60.seconds) + catch + case e: Exception => Assertions.fail("Vastaanottaja ei muuttunut delivery-tilaan sallitussa ajassa") + + Assertions.assertEquals(SAHKOPOSTIOSOITE_EI_VALIDI_ERROR, siirtyma.lisatiedot) } diff --git a/lahetysrajapinta/src/main/scala/fi/oph/viestinvalitys/vastaanotto/model/Lahetys.scala b/lahetysrajapinta/src/main/scala/fi/oph/viestinvalitys/vastaanotto/model/Lahetys.scala index 8f200b57..5b881266 100644 --- a/lahetysrajapinta/src/main/scala/fi/oph/viestinvalitys/vastaanotto/model/Lahetys.scala +++ b/lahetysrajapinta/src/main/scala/fi/oph/viestinvalitys/vastaanotto/model/Lahetys.scala @@ -21,7 +21,7 @@ case class LahettajaImpl( @(Schema @field)(example = "Opintopolku", maxLength = Viesti.VIESTI_NIMI_MAX_PITUUS) @BeanProperty nimi: Optional[String], - @(Schema @field)(description="Domainin pitää olla opintopolku.fi", example = "noreply@opintopolku.fi", requiredMode=RequiredMode.REQUIRED) + @(Schema @field)(description="Domainin pitää olla opintopolku.fi", example = "noreply@opintopolku.fi", requiredMode=RequiredMode.REQUIRED, maxLength = Viesti.VIESTI_OSOITE_MAX_PITUUS) @BeanProperty sahkopostiOsoite: Optional[String], ) extends Lahettaja { diff --git a/lahetysrajapinta/src/main/scala/fi/oph/viestinvalitys/vastaanotto/model/Viesti.java b/lahetysrajapinta/src/main/scala/fi/oph/viestinvalitys/vastaanotto/model/Viesti.java index 8285d1f1..7e2d1135 100644 --- a/lahetysrajapinta/src/main/scala/fi/oph/viestinvalitys/vastaanotto/model/Viesti.java +++ b/lahetysrajapinta/src/main/scala/fi/oph/viestinvalitys/vastaanotto/model/Viesti.java @@ -33,6 +33,7 @@ public interface Viesti { * Viestin lähettäjän ja yksittäisten vastaanottajien nimien maksimipituus. */ static final int VIESTI_NIMI_MAX_PITUUS = 64; + static final int VIESTI_OSOITE_MAX_PITUUS = 512; static final int VIESTI_SALAISUUS_MIN_PITUUS = 8; static final int VIESTI_SALAISUUS_MAX_PITUUS = 1024; diff --git a/lahetysrajapinta/src/main/scala/fi/oph/viestinvalitys/vastaanotto/model/Viesti.scala b/lahetysrajapinta/src/main/scala/fi/oph/viestinvalitys/vastaanotto/model/Viesti.scala index b4b7bbcb..6efd6e87 100644 --- a/lahetysrajapinta/src/main/scala/fi/oph/viestinvalitys/vastaanotto/model/Viesti.scala +++ b/lahetysrajapinta/src/main/scala/fi/oph/viestinvalitys/vastaanotto/model/Viesti.scala @@ -26,7 +26,7 @@ case class VastaanottajaImpl( @(Schema @field)(example = "Vallu Vastaanottaja", maxLength = Viesti.VIESTI_NIMI_MAX_PITUUS) @BeanProperty nimi: Optional[String], - @(Schema @field)(example = "vallu.vastaanottaja@example.com", requiredMode=RequiredMode.REQUIRED) + @(Schema @field)(example = "vallu.vastaanottaja@example.com", requiredMode=RequiredMode.REQUIRED, maxLength = Viesti.VIESTI_OSOITE_MAX_PITUUS) @BeanProperty sahkopostiOsoite: Optional[String], ) extends Vastaanottaja { diff --git a/lambdat/lahetys/pom.xml b/lambdat/lahetys/pom.xml index a3f797c5..363202c6 100644 --- a/lambdat/lahetys/pom.xml +++ b/lambdat/lahetys/pom.xml @@ -58,6 +58,10 @@ net.logstash.logback logstash-logback-encoder + + commons-validator + commons-validator + diff --git a/lambdat/lahetys/src/main/scala/fi/oph/viestinvalitys/lahetys/LambdaHandler.scala b/lambdat/lahetys/src/main/scala/fi/oph/viestinvalitys/lahetys/LambdaHandler.scala index e02da5f9..727e5093 100644 --- a/lambdat/lahetys/src/main/scala/fi/oph/viestinvalitys/lahetys/LambdaHandler.scala +++ b/lambdat/lahetys/src/main/scala/fi/oph/viestinvalitys/lahetys/LambdaHandler.scala @@ -20,6 +20,7 @@ import software.amazon.awssdk.core.SdkBytes import software.amazon.awssdk.services.cloudwatch.model.{Dimension, MetricDatum, PutMetricDataRequest, StandardUnit} import software.amazon.awssdk.services.s3.model.GetObjectRequest import software.amazon.awssdk.services.ses.model.{RawMessage, SendRawEmailRequest} +import org.apache.commons.validator.routines.EmailValidator import java.io.ByteArrayOutputStream import java.time.Instant @@ -28,6 +29,8 @@ import java.util.UUID import scala.jdk.CollectionConverters.{CollectionHasAsScala, SeqHasAsJava} object LambdaHandler { + val SAHKOPOSTIOSOITE_EI_VALIDI_ERROR = "Sähköpostiosoite ei validi" + val LOG = LoggerFactory.getLogger(classOf[LambdaHandler]); val queueUrl = ConfigurationUtil.getConfigurationItem(ConfigurationUtil.AJASTUS_QUEUE_URL_KEY).get; val kantaOperaatiot = new KantaOperaatiot(DbUtil.database) @@ -97,60 +100,70 @@ class LambdaHandler extends RequestHandler[SQSEvent, Void], Resource { LogContext(vastaanottajaTunniste = vastaanottaja.tunniste.toString, viestiTunniste = vastaanottaja.viestiTunniste.toString)(() => { try { LOG.info(s"Lähetetään viestiä vastaanottajalle ${vastaanottaja.tunniste.toString}") - val viesti = viestit.getOrElseUpdate(vastaanottaja.viestiTunniste, kantaOperaatiot.getViestit(Seq(vastaanottaja.viestiTunniste)).find(v => true).get) - - var builder = EmailBuilder.startingBlank() - .withContentTransferEncoding(ContentTransferEncoding.BASE_64) - .withSubject(viesti.otsikko) - - if (viesti.replyTo.isDefined) - builder.withReplyTo(viesti.replyTo.get) - - viesti.sisallonTyyppi match { - case SisallonTyyppi.TEXT => builder = builder.withPlainText(viesti.sisalto) - case SisallonTyyppi.HTML => builder = builder.withHTMLText(viesti.sisalto) - } - - liitteet.getOrElseUpdate(viesti.tunniste, kantaOperaatiot.getViestinLiitteet(Seq(viesti.tunniste)) - .find((viestiTunniste, liitteet) => true).map((viestiTunniste, liitteet) => liitteet.map(liite => { - val getObjectResponse = AwsUtil.s3Client.getObject(GetObjectRequest - .builder() - .bucket(bucketName) - .key(liite.tunniste.toString) + + if(!EmailValidator.getInstance().isValid(vastaanottaja.kontakti.sahkoposti)) + LOG.error(s"Vastaanottajan ${vastaanottaja.tunniste.toString} sähköposti ei ole validi, siirretään suoraan virhetilaan") + val changes: Changes = new Changes.Builder() + .added("lisatiedot", SAHKOPOSTIOSOITE_EI_VALIDI_ERROR) + .updated("vastaanottajanTila", vastaanottaja.tila.toString, VastaanottajanTila.VIRHE.toString) + .build() + AuditLog.logChanges(AuditLog.getAuditUserForLambda(), Map("vastaanottaja" -> vastaanottaja.tunniste.toString), AuditOperation.UpdateVastaanottajanTila, changes) + kantaOperaatiot.paivitaVastaanottajaVirhetilaan(vastaanottaja.tunniste, SAHKOPOSTIOSOITE_EI_VALIDI_ERROR) + else + val viesti = viestit.getOrElseUpdate(vastaanottaja.viestiTunniste, kantaOperaatiot.getViestit(Seq(vastaanottaja.viestiTunniste)).find(v => true).get) + + var builder = EmailBuilder.startingBlank() + .withContentTransferEncoding(ContentTransferEncoding.BASE_64) + .withSubject(viesti.otsikko) + + if (viesti.replyTo.isDefined) + builder.withReplyTo(viesti.replyTo.get) + + viesti.sisallonTyyppi match { + case SisallonTyyppi.TEXT => builder = builder.withPlainText(viesti.sisalto) + case SisallonTyyppi.HTML => builder = builder.withHTMLText(viesti.sisalto) + } + + liitteet.getOrElseUpdate(viesti.tunniste, kantaOperaatiot.getViestinLiitteet(Seq(viesti.tunniste)) + .find((viestiTunniste, liitteet) => true).map((viestiTunniste, liitteet) => liitteet.map(liite => { + val getObjectResponse = AwsUtil.s3Client.getObject(GetObjectRequest + .builder() + .bucket(bucketName) + .key(liite.tunniste.toString) + .build()) + (liite, getObjectResponse.readAllBytes) + })).getOrElse(Seq.empty)).foreach((liite, bytes) => { + builder = builder.withAttachment(liite.nimi, bytes, liite.contentType) + }) + + val sesTunniste = { + if (mode == Mode.PRODUCTION) + this.sendSesEmail(builder + .from(viesti.lahettaja.nimi.getOrElse(null), viesti.lahettaja.sahkoposti) + .to(vastaanottaja.kontakti.nimi.getOrElse(null), vastaanottaja.kontakti.sahkoposti) + .buildEmail()) + else + sendTestEmail(vastaanottaja, builder.from(viesti.lahettaja.nimi.getOrElse(null), s"noreply@${ConfigurationUtil.opintopolkuDomain}")) + } + val changes: Changes = new Changes.Builder() + .added("sesTunniste", sesTunniste) + .updated("vastaanottajanTila",vastaanottaja.tila.toString, VastaanottajanTila.LAHETETTY.toString) + .build() + AuditLog.logChanges(AuditLog.getAuditUserForLambda(), Map("vastaanottaja" -> vastaanottaja.tunniste.toString), AuditOperation.SendEmail, changes) + LOG.info(s"Lähetetty viesti vastaanottajalle ${vastaanottaja.tunniste.toString}") + kantaOperaatiot.paivitaVastaanottajaLahetetyksi(vastaanottaja.tunniste, sesTunniste) + + metricDatums.add(MetricDatum.builder() + .metricName("LahetyksienMaara") + .value(1) + .storageResolution(1) + .dimensions(Seq(Dimension.builder() + .name("Prioriteetti") + .value(viesti.prioriteetti.toString) + .build()).asJava) + .timestamp(Instant.now()) + .unit(StandardUnit.COUNT) .build()) - (liite, getObjectResponse.readAllBytes) - })).getOrElse(Seq.empty)).foreach((liite, bytes) => { - builder = builder.withAttachment(liite.nimi, bytes, liite.contentType) - }) - - val sesTunniste = { - if (mode == Mode.PRODUCTION) - this.sendSesEmail(builder - .from(viesti.lahettaja.nimi.getOrElse(null), viesti.lahettaja.sahkoposti) - .to(vastaanottaja.kontakti.nimi.getOrElse(null), vastaanottaja.kontakti.sahkoposti) - .buildEmail()) - else - sendTestEmail(vastaanottaja, builder.from(viesti.lahettaja.nimi.getOrElse(null), s"noreply@${ConfigurationUtil.opintopolkuDomain}")) - } - val changes: Changes = new Changes.Builder() - .added("sesTunniste", sesTunniste) - .updated("vastaanottajanTila",vastaanottaja.tila.toString, VastaanottajanTila.LAHETETTY.toString) - .build() - AuditLog.logChanges(AuditLog.getAuditUserForLambda(), Map("vastaanottaja" -> vastaanottaja.tunniste.toString), AuditOperation.SendEmail, changes) - LOG.info(s"Lähetetty viesti vastaanottajalle ${vastaanottaja.tunniste.toString}") - kantaOperaatiot.paivitaVastaanottajaLahetetyksi(vastaanottaja.tunniste, sesTunniste) - - metricDatums.add(MetricDatum.builder() - .metricName("LahetyksienMaara") - .value(1) - .storageResolution(1) - .dimensions(Seq(Dimension.builder() - .name("Prioriteetti") - .value(viesti.prioriteetti.toString) - .build()).asJava) - .timestamp(Instant.now()) - .unit(StandardUnit.COUNT) - .build()) } catch { case e: Exception => LOG.error(s"Virhe lähetettäessä viestiä vastaanottajalle ${vastaanottaja.tunniste.toString}", e) diff --git a/lambdat/vastaanotto/src/main/scala/fi/oph/viestinvalitys/vastaanotto/validation/ViestiValidator.scala b/lambdat/vastaanotto/src/main/scala/fi/oph/viestinvalitys/vastaanotto/validation/ViestiValidator.scala index 9403681e..b02e48d1 100644 --- a/lambdat/vastaanotto/src/main/scala/fi/oph/viestinvalitys/vastaanotto/validation/ViestiValidator.scala +++ b/lambdat/vastaanotto/src/main/scala/fi/oph/viestinvalitys/vastaanotto/validation/ViestiValidator.scala @@ -4,7 +4,6 @@ import fi.oph.viestinvalitys.vastaanotto.model.Lahetys.* import fi.oph.viestinvalitys.vastaanotto.model.LahetysImpl.LAHETYS_PRIORITEETTI_KORKEA import fi.oph.viestinvalitys.vastaanotto.model.Viesti.* import fi.oph.viestinvalitys.vastaanotto.validation.LahetysValidator -import org.apache.commons.validator.routines.EmailValidator import java.util.stream.Collectors import java.util.{List, Optional, UUID} @@ -52,7 +51,7 @@ object ViestiValidator: final val VALIDATION_VASTAANOTTAJA_OSOITE_DUPLICATE = "vastaanottajat: Osoite-kentissä on duplikaatteja: " final val VALIDATION_VASTAANOTTAJAN_NIMI_LIIAN_PITKA = "nimi-kenttä voi maksimissaan olla " + VIESTI_NIMI_MAX_PITUUS + " merkkiä pitkä" final val VALIDATION_VASTAANOTTAJAN_OSOITE_TYHJA = "sähköpostiosoite-kenttä on pakollinen" - final val VALIDATION_VASTAANOTTAJAN_OSOITE_INVALID = "sähköpostiosoite ei ole validi sähköpostiosoite" + final val VALIDATION_VASTAANOTTAJAN_OSOITE_LIIAN_PITKA = "sähköpostiosoite void maksimissaan olla " + " merkkiä pitkä" final val VALIDATION_LIITETUNNISTE_NULL = "liiteTunnisteet: Kenttä sisältää null-arvoja" final val VALIDATION_LIITETUNNISTE_LIIKAA = "liiteTunnisteet: Viestillä voi maksimissaan olla " + VIESTI_LIITTEET_MAX_MAARA + " liitettä" @@ -214,8 +213,8 @@ object ViestiValidator: // validoidaan sahkopostiosoite if (vastaanottaja.getSahkopostiOsoite.isEmpty || vastaanottaja.getSahkopostiOsoite.get.length == 0) vastaanottajaVirheet.incl(VALIDATION_VASTAANOTTAJAN_OSOITE_TYHJA) - else if (!EmailValidator.getInstance().isValid(vastaanottaja.getSahkopostiOsoite.get)) - vastaanottajaVirheet.incl(VALIDATION_VASTAANOTTAJAN_OSOITE_INVALID) + else if (vastaanottaja.getSahkopostiOsoite.get().length>Viesti.VIESTI_OSOITE_MAX_PITUUS) + vastaanottajaVirheet.incl(VALIDATION_VASTAANOTTAJAN_OSOITE_LIIAN_PITKA) else vastaanottajaVirheet).get if (!vastaanottajaVirheet.isEmpty) diff --git a/lambdat/vastaanotto/src/test/scala/fi/oph/viestinvalitys/vastaanotto/validation/ViestiValidatorTest.scala b/lambdat/vastaanotto/src/test/scala/fi/oph/viestinvalitys/vastaanotto/validation/ViestiValidatorTest.scala index f8e77e80..80d0bf53 100644 --- a/lambdat/vastaanotto/src/test/scala/fi/oph/viestinvalitys/vastaanotto/validation/ViestiValidatorTest.scala +++ b/lambdat/vastaanotto/src/test/scala/fi/oph/viestinvalitys/vastaanotto/validation/ViestiValidatorTest.scala @@ -176,17 +176,17 @@ class ViestiValidatorTest { Assertions.assertEquals(Set("Vastaanottaja (nimi: Vallu Vastaanottaja, sähköpostiosoite: Optional.empty): " + ViestiValidator.VALIDATION_VASTAANOTTAJAN_OSOITE_TYHJA), ViestiValidator.validateVastaanottajat(Optional.of(util.List.of(getVastaanottaja("Vallu Vastaanottaja", null))))) - // ei validi sähköpostiosoite ei ole sallittu - Assertions.assertEquals(Set("Vastaanottaja (nimi: Vallu Vastaanottaja, sähköpostiosoite: Optional[ei validi osoite]): " + ViestiValidator.VALIDATION_VASTAANOTTAJAN_OSOITE_INVALID), - ViestiValidator.validateVastaanottajat(Optional.of(util.List.of(getVastaanottaja("Vallu Vastaanottaja", "ei validi osoite"))))) + // liian pitkä sähköpostiosoite ei ole sallittu + Assertions.assertEquals(Set("Vastaanottaja (nimi: Vallu Vastaanottaja, sähköpostiosoite: Optional[" + "x".repeat(Viesti.VIESTI_OSOITE_MAX_PITUUS+1) + "]): " + ViestiValidator.VALIDATION_VASTAANOTTAJAN_OSOITE_LIIAN_PITKA), + ViestiValidator.validateVastaanottajat(Optional.of(util.List.of(getVastaanottaja("Vallu Vastaanottaja", "x".repeat(Viesti.VIESTI_OSOITE_MAX_PITUUS+1)))))) // kaikki virheet kerätään Assertions.assertEquals(Set( "Vastaanottaja (nimi: Vallu Vastaanottaja, sähköpostiosoite: Optional.empty): " + ViestiValidator.VALIDATION_VASTAANOTTAJAN_OSOITE_TYHJA, - "Vastaanottaja (nimi: Vallu Vastaanottaja, sähköpostiosoite: Optional[ei validi osoite]): " + ViestiValidator.VALIDATION_VASTAANOTTAJAN_OSOITE_INVALID), + "Vastaanottaja (nimi: Vallu Vastaanottaja, sähköpostiosoite: Optional[" + "x".repeat(Viesti.VIESTI_OSOITE_MAX_PITUUS+1) + "]): " + ViestiValidator.VALIDATION_VASTAANOTTAJAN_OSOITE_LIIAN_PITKA), ViestiValidator.validateVastaanottajat(Optional.of(util.List.of( getVastaanottaja("Vallu Vastaanottaja", null), - getVastaanottaja("Vallu Vastaanottaja", "ei validi osoite") + getVastaanottaja("Vallu Vastaanottaja", "x".repeat(Viesti.VIESTI_OSOITE_MAX_PITUUS+1)) )))) // duplikaattiosoitteet eivät ole sallittuja