diff --git a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala index 3bc993f9..5c4a01a1 100644 --- a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala +++ b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala @@ -170,13 +170,6 @@ object JoexAppImpl extends MailAddressCodec { ReProcessItem.onCancel[F] ) ) - .withTask( - JobTask.json( - NotifyDueItemsArgs.taskName, - NotifyDueItemsTask[F](cfg.sendMail, javaEmil), - NotifyDueItemsTask.onCancel[F] - ) - ) .withTask( JobTask.json( ScanMailboxArgs.taskName, diff --git a/modules/joex/src/main/scala/docspell/joex/notify/MailContext.scala b/modules/joex/src/main/scala/docspell/joex/notify/MailContext.scala deleted file mode 100644 index 55a8b991..00000000 --- a/modules/joex/src/main/scala/docspell/joex/notify/MailContext.scala +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020 Eike K. & Contributors - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package docspell.joex.notify - -import docspell.common._ -import docspell.joex.notify.YamuscaConverter._ -import docspell.store.queries.ListItem - -import yamusca.implicits._ -import yamusca.imports._ - -/** The context for rendering the e-mail template. */ -case class MailContext( - items: List[MailContext.ItemData], - more: Boolean, - account: AccountId, - username: String, - itemUri: Option[LenientUri] -) - -object MailContext { - - def from( - items: Vector[ListItem], - max: Int, - account: AccountId, - itemBaseUri: Option[LenientUri], - now: Timestamp - ): MailContext = - MailContext( - items.take(max - 1).map(ItemData(now)).toList.sortBy(_.dueDate), - items.sizeCompare(max) >= 0, - account, - account.user.id.capitalize, - itemBaseUri - ) - - case class ItemData( - id: Ident, - name: String, - date: Timestamp, - dueDate: Option[Timestamp], - source: String, - overDue: Boolean, - dueIn: Option[String], - corrOrg: Option[String] - ) - - object ItemData { - - def apply(now: Timestamp)(i: ListItem): ItemData = { - val dueIn = i.dueDate.map(dt => Timestamp.daysBetween(now, dt)) - val dueInLabel = dueIn.map { - case 0 => "**today**" - case 1 => "**tomorrow**" - case -1 => s"**yesterday**" - case n if n > 0 => s"in $n days" - case n => s"${n * -1} days ago" - } - ItemData( - i.id, - i.name, - i.date, - i.dueDate, - i.source, - dueIn.exists(_ < 0), - dueInLabel, - i.corrOrg.map(_.name) - ) - } - - implicit def yamusca: ValueConverter[ItemData] = - ValueConverter.deriveConverter[ItemData] - } - - implicit val yamusca: ValueConverter[MailContext] = - ValueConverter.deriveConverter[MailContext] - -} diff --git a/modules/joex/src/main/scala/docspell/joex/notify/MailTemplate.scala b/modules/joex/src/main/scala/docspell/joex/notify/MailTemplate.scala deleted file mode 100644 index edd3f4e9..00000000 --- a/modules/joex/src/main/scala/docspell/joex/notify/MailTemplate.scala +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 Eike K. & Contributors - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package docspell.joex.notify - -import yamusca.implicits._ - -object MailTemplate { - - val text = mustache""" -Hello {{{ username }}}, - -this is Docspell informing you about your next due items coming up. - -{{#itemUri}} -{{#items}} -- {{#overDue}}**(OVERDUE)** {{/overDue}}[{{name}}]({{itemUri}}/{{id}}), - {{#overDue}}was {{/overDue}}due {{dueIn}} on *{{dueDate}}*; {{#corrOrg}}from {{corrOrg}}{{/corrOrg}} - received on {{date}} via {{source}} -{{/items}} -{{/itemUri}} -{{^itemUri}} -{{#items}} -- {{#overDue}}**(OVERDUE)** {{/overDue}}*{{name}}*, - {{#overDue}}was {{/overDue}}due {{dueIn}} on *{{dueDate}}*; {{#corrOrg}}from {{corrOrg}}{{/corrOrg}} - received on {{date}} via {{source}} -{{/items}} -{{/itemUri}} -{{#more}} -- … more have been left out for brevity -{{/more}} - - -Sincerely yours, - -Docspell -""" - - def render(mc: MailContext): String = - mc.render(text) -} diff --git a/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala b/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala deleted file mode 100644 index 508e40cc..00000000 --- a/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2020 Eike K. & Contributors - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package docspell.joex.notify - -import cats.data.{NonEmptyList => Nel, OptionT} -import cats.effect._ -import cats.implicits._ - -import docspell.backend.ops.OItemSearch.{Batch, ListItem, Query} -import docspell.common._ -import docspell.joex.mail.EmilHeader -import docspell.joex.scheduler.{Context, Task} -import docspell.query.Date -import docspell.query.ItemQuery._ -import docspell.query.ItemQueryDsl._ -import docspell.store.queries.QItem -import docspell.store.records._ - -import emil._ -import emil.builder._ -import emil.javamail.syntax._ -import emil.markdown._ - -object NotifyDueItemsTask { - val maxItems: Int = 7 - type Args = NotifyDueItemsArgs - - def apply[F[_]: Sync](cfg: MailSendConfig, emil: Emil[F]): Task[F, Args, Unit] = - Task { ctx => - for { - _ <- ctx.logger.info("Getting mail configuration") - mailCfg <- getMailSettings(ctx) - _ <- ctx.logger.info( - s"Searching for items due in ${ctx.args.remindDays} days…." - ) - _ <- createMail(cfg, mailCfg, ctx) - .semiflatMap { mail => - for { - _ <- ctx.logger.info(s"Sending notification mail to ${ctx.args.recipients}") - res <- emil(mailCfg.toMailConfig).send(mail).map(_.head) - _ <- ctx.logger.info(s"Sent mail with id: $res") - } yield () - } - .getOrElseF(ctx.logger.info("No items found")) - } yield () - } - - def onCancel[F[_]]: Task[F, NotifyDueItemsArgs, Unit] = - Task.log(_.warn("Cancelling notify-due-items task")) - - def getMailSettings[F[_]: Sync](ctx: Context[F, Args]): F[RUserEmail] = - ctx.store - .transact(RUserEmail.getByName(ctx.args.account, ctx.args.smtpConnection)) - .flatMap { - case Some(c) => c.pure[F] - case None => - Sync[F].raiseError( - new Exception( - s"No smtp configuration found for: ${ctx.args.smtpConnection.id}" - ) - ) - } - - def createMail[F[_]: Sync]( - sendCfg: MailSendConfig, - cfg: RUserEmail, - ctx: Context[F, Args] - ): OptionT[F, Mail[F]] = - for { - items <- OptionT.liftF(findItems(ctx)).filter(_.nonEmpty) - mail <- OptionT.liftF(makeMail(sendCfg, cfg, ctx.args, items)) - } yield mail - - def findItems[F[_]: Sync](ctx: Context[F, Args]): F[Vector[ListItem]] = - for { - now <- Timestamp.current[F] - rightDate = Date((now + Duration.days(ctx.args.remindDays.toLong)).toMillis) - q = - Query - .all(ctx.args.account) - .withOrder(orderAsc = _.dueDate) - .withFix(_.copy(query = Expr.ValidItemStates.some)) - .withCond(_ => - Query.QueryExpr( - Attr.DueDate <= rightDate &&? - ctx.args.daysBack.map(back => - Attr.DueDate >= Date((now - Duration.days(back.toLong)).toMillis) - ) &&? - Nel - .fromList(ctx.args.tagsInclude) - .map(ids => Q.tagIdsEq(ids.map(_.id))) &&? - Nel - .fromList(ctx.args.tagsExclude) - .map(ids => Q.tagIdsIn(ids.map(_.id)).negate) - ) - ) - res <- - ctx.store - .transact( - QItem - .findItems(q, now.toUtcDate, 0, Batch.limit(maxItems)) - .take(maxItems.toLong) - ) - .compile - .toVector - } yield res - - def makeMail[F[_]: Sync]( - sendCfg: MailSendConfig, - cfg: RUserEmail, - args: Args, - items: Vector[ListItem] - ): F[Mail[F]] = - Timestamp.current[F].map { now => - val templateCtx = - MailContext.from(items, maxItems.toInt, args.account, args.itemDetailUrl, now) - val md = MailTemplate.render(templateCtx) - val recp = args.recipients - .map(MailAddress.parse) - .map { - case Right(ma) => ma - case Left(err) => - throw new Exception(s"Unable to parse recipient address: $err") - } - MailBuilder.build( - From(cfg.mailFrom), - Tos(recp), - XMailer.emil, - Subject("[Docspell] Next due items"), - EmilHeader.listId(sendCfg.listId), - MarkdownBody[F](md).withConfig( - MarkdownConfig("body { font-size: 10pt; font-family: sans-serif; }") - ) - ) - } -} diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicDueItemsArgs.scala b/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicDueItemsArgs.scala index 2a34ec26..fc10767f 100644 --- a/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicDueItemsArgs.scala +++ b/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicDueItemsArgs.scala @@ -12,6 +12,13 @@ import emil.MailAddress import io.circe.generic.semiauto import io.circe.{Decoder, Encoder} +/** Arguments to the notification task. + * + * This tasks queries items with a due date and informs the user via mail. + * + * If the structure changes, there must be some database migration to update or remove + * the json data of the corresponding task. + */ final case class PeriodicDueItemsArgs( account: AccountId, channel: ChannelOrRef, diff --git a/modules/common/src/main/scala/docspell/common/NotifyDueItemsArgs.scala b/modules/store/src/main/scala/db/migration/NotifyDueItemsArgs.scala similarity index 87% rename from modules/common/src/main/scala/docspell/common/NotifyDueItemsArgs.scala rename to modules/store/src/main/scala/db/migration/NotifyDueItemsArgs.scala index ff3ff9b1..d80fced5 100644 --- a/modules/common/src/main/scala/docspell/common/NotifyDueItemsArgs.scala +++ b/modules/store/src/main/scala/db/migration/NotifyDueItemsArgs.scala @@ -17,6 +17,9 @@ import io.circe.generic.semiauto._ * * If the structure changes, there must be some database migration to update or remove * the json data of the corresponding task. + * + * @deprecated note: This has been removed and copied to this place to be able to + * migrate away from this structure. Replaced by PeriodicDueItemsArgs */ case class NotifyDueItemsArgs( account: AccountId,