diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala index 80875f92..5fad31bd 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala @@ -6,17 +6,13 @@ import cats.implicits._ import cats.data.OptionT import emil._ import emil.javamail.syntax._ +import bitpeace.{FileMeta, RangeDef} import docspell.common._ import docspell.store._ -import docspell.store.records.RUserEmail -import OMail.{ItemMail, SmtpSettings} -import docspell.store.records.RAttachment -import bitpeace.FileMeta -import bitpeace.RangeDef -import docspell.store.records.RItem -import docspell.store.records.RSentMail -import docspell.store.records.RSentMailItem +import docspell.store.records._ +import docspell.store.queries.QMails +import OMail.{ItemMail, Sent, SmtpSettings} trait OMail[F[_]] { @@ -31,10 +27,32 @@ trait OMail[F[_]] { def deleteSettings(accId: AccountId, name: Ident): F[Int] def sendMail(accId: AccountId, name: Ident, m: ItemMail): F[SendResult] + + def getSentMailsForItem(accId: AccountId, itemId: Ident): F[Vector[Sent]] + + def getSentMail(accId: AccountId, mailId: Ident): OptionT[F, Sent] + + def deleteSentMail(accId: AccountId, mailId: Ident): F[Int] } object OMail { + case class Sent( + id: Ident, + senderLogin: Ident, + connectionName: Ident, + recipients: List[MailAddress], + subject: String, + body: String, + created: Timestamp + ) + + object Sent { + + def create(r: RSentMail, login: Ident): Sent = + Sent(r.id, login, r.connName, r.recipients, r.subject, r.body, r.created) + } + case class ItemMail( item: Ident, subject: String, @@ -155,6 +173,7 @@ object OMail { accId, msgId, cfg.mailFrom, + name, m.subject, m.recipients, m.body @@ -180,5 +199,17 @@ object OMail { } yield conv).getOrElse(SendResult.NotFound) } + def getSentMailsForItem(accId: AccountId, itemId: Ident): F[Vector[Sent]] = + store + .transact(QMails.findMails(accId.collective, itemId)) + .map(_.map(t => Sent.create(t._1, t._2))) + + def getSentMail(accId: AccountId, mailId: Ident): OptionT[F, Sent] = + OptionT(store.transact(QMails.findMail(accId.collective, mailId))).map(t => + Sent.create(t._1, t._2) + ) + + def deleteSentMail(accId: AccountId, mailId: Ident): F[Int] = + store.transact(QMails.delete(accId.collective, mailId)) }) } diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 887c4ac4..2e9a69e3 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -125,6 +125,8 @@ paths: The result shows all items that contains a file with the given checksum. + security: + - authTokenHeader: [] parameters: - $ref: "#/components/parameters/checksum" responses: @@ -159,6 +161,8 @@ paths: * application/pdf Support for more types might be added. + security: + - authTokenHeader: [] requestBody: content: multipart/form-data: @@ -1188,6 +1192,8 @@ paths: Get the current state of the job qeue. The job qeue contains all processing tasks and other long-running operations. All users/collectives share processing resources. + security: + - authTokenHeader: [] responses: 200: description: Ok @@ -1203,6 +1209,8 @@ paths: Tries to cancel a job and remove it from the queue. If the job is running, a cancel request is send to the corresponding joex instance. Otherwise the job is removed from the queue. + security: + - authTokenHeader: [] parameters: - $ref: "#/components/parameters/id" responses: @@ -1224,6 +1232,8 @@ paths: Multiple e-mail settings can be specified, they are distinguished by their `name`. The query `q` parameter does a simple substring search in the connection name. + security: + - authTokenHeader: [] parameters: - $ref: "#/components/parameters/q" responses: @@ -1238,6 +1248,8 @@ paths: summary: Create new email settings description: | Create new e-mail settings. + security: + - authTokenHeader: [] requestBody: content: application/json: @@ -1259,6 +1271,8 @@ paths: description: | Return the stored e-mail settings for the given connection name. + security: + - authTokenHeader: [] responses: 200: description: Ok @@ -1271,6 +1285,8 @@ paths: summary: Change specific email settings. description: | Changes all settings for the connection with the given `name`. + security: + - authTokenHeader: [] requestBody: content: application/json: @@ -1288,6 +1304,8 @@ paths: summary: Delete e-mail settings. description: | Deletes the e-mail settings with the specified `name`. + security: + - authTokenHeader: [] responses: 200: description: Ok @@ -1301,10 +1319,11 @@ paths: tags: [ E-Mail ] summary: Send an email. description: | - Sends an email as specified with all attachments of the item - with `id` as mail attachments. If the item has no attachments, - then the mail is sent without any. If the item's attachments - exceed a specific size, the mail will not be sent. + Sends an email as specified in the body of the request. + + The item's attachment are added to the mail if requested. + security: + - authTokenHeader: [] parameters: - $ref: "#/components/parameters/name" - $ref: "#/components/parameters/id" @@ -1320,9 +1339,100 @@ paths: application/json: schema: $ref: "#/components/schemas/BasicResult" + /sec/email/sent/item/{id}: + get: + tags: [ E-Mail ] + summary: Get sent mail related to an item + description: | + Return all mails that have been sent related to the item with + id `id`. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/SentMails" + /sec/email/sent/mail/{mailId}: + parameters: + - $ref: "#/components/parameters/mailId" + get: + tags: [ E-Mail ] + summary: Get sent single mail related to an item + description: | + Return one mail with the given id. + security: + - authTokenHeader: [] + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/SentMail" + delete: + tags: [ E-Mail ] + summary: Delete a sent mail. + description: | + Delete a sent mail. + security: + - authTokenHeader: [] + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" components: schemas: + SentMails: + description: | + A list of sent mails. + required: + - items + properties: + items: + type: array + items: + $ref: "#/components/schemas/SentMail" + SentMail: + description: | + A mail that has been sent previously related to an item. + required: + - id + - sender + - connection + - recipients + - subject + - body + - created + properties: + id: + type: string + format: ident + sender: + type: string + format: ident + connection: + type: string + format: ident + recipients: + type: array + items: + type: string + subject: + type: string + body: + type: string + created: + type: integer + format: date-time SimpleMail: description: | A simple e-mail related to an item. @@ -1330,7 +1440,8 @@ components: The mail may contain the item attachments as mail attachments. If all item attachments should be send, set `addAllAttachments` to `true`. Otherwise set it to `false` and - specify a list of file-ids that you want to include. + specify a list of file-ids that you want to include. This list + is ignored, if `addAllAttachments` is set to `true`. required: - recipients - subject @@ -2369,3 +2480,10 @@ components: required: true schema: type: string + mailId: + name: mailId + in: path + description: The id of a sent mail. + required: true + schema: + type: string diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala index f9eb1c1b..160a92a7 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala @@ -71,7 +71,8 @@ object RestServer { "upload" -> UploadRoutes.secured(restApp.backend, cfg, token), "checkfile" -> CheckFileRoutes.secured(restApp.backend, token), "email/send" -> MailSendRoutes(restApp.backend, token), - "email/settings" -> MailSettingsRoutes(restApp.backend, token) + "email/settings" -> MailSettingsRoutes(restApp.backend, token), + "email/sent" -> SentMailRoutes(restApp.backend, token) ) def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] = diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/SentMailRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/SentMailRoutes.scala new file mode 100644 index 00000000..01f22c45 --- /dev/null +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/SentMailRoutes.scala @@ -0,0 +1,54 @@ +package docspell.restserver.routes + +import cats.effect._ +import cats.implicits._ +import cats.data.OptionT +import org.http4s._ +import org.http4s.dsl.Http4sDsl +import org.http4s.circe.CirceEntityEncoder._ + +import docspell.backend.BackendApp +import docspell.backend.auth.AuthToken +import docspell.backend.ops.OMail.Sent +import docspell.common._ +import docspell.restapi.model._ +import docspell.store.EmilUtil + +object SentMailRoutes { + + def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = { + val dsl = new Http4sDsl[F] {} + import dsl._ + + HttpRoutes.of { + case GET -> Root / "item" / Ident(id) => + for { + all <- backend.mail.getSentMailsForItem(user.account, id) + resp <- Ok(SentMails(all.map(convert).toList)) + } yield resp + + case GET -> Root / "mail" / Ident(mailId) => + (for { + mail <- backend.mail.getSentMail(user.account, mailId) + resp <- OptionT.liftF(Ok(convert(mail))) + } yield resp).getOrElseF(NotFound()) + + case DELETE -> Root / "mail" / Ident(mailId) => + for { + n <- backend.mail.deleteSentMail(user.account, mailId) + resp <- Ok(BasicResult(n > 0, s"Mails deleted: $n")) + } yield resp + } + } + + def convert(s: Sent): SentMail = + SentMail( + s.id, + s.senderLogin, + s.connectionName, + s.recipients.map(EmilUtil.mailAddressString), + s.subject, + s.body, + s.created + ) +} diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.1.0__useremail.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.1.0__useremail.sql index 5cf5f9f1..75812938 100644 --- a/modules/store/src/main/resources/db/migration/postgresql/V1.1.0__useremail.sql +++ b/modules/store/src/main/resources/db/migration/postgresql/V1.1.0__useremail.sql @@ -20,6 +20,7 @@ CREATE TABLE "sentmail" ( "uid" varchar(254) not null, "message_id" varchar(254) not null, "sender" varchar(254) not null, + "conn_name" varchar(254) not null, "subject" varchar(254) not null, "recipients" varchar(254) not null, "body" text not null, diff --git a/modules/store/src/main/scala/docspell/store/impl/DoobieSyntax.scala b/modules/store/src/main/scala/docspell/store/impl/DoobieSyntax.scala index 47e61345..4eef507f 100644 --- a/modules/store/src/main/scala/docspell/store/impl/DoobieSyntax.scala +++ b/modules/store/src/main/scala/docspell/store/impl/DoobieSyntax.scala @@ -65,12 +65,6 @@ trait DoobieSyntax { Fragment.const("SELECT DISTINCT(") ++ commas(cols.map(_.f)) ++ Fragment.const(") FROM ") ++ table ++ this.where(where) -// def selectJoinCollective(cols: Seq[Column], fkCid: Column, table: Fragment, wh: Fragment): Fragment = -// selectSimple(cols.map(_.prefix("a")) -// , table ++ fr"a," ++ RCollective.table ++ fr"b" -// , if (isEmpty(wh)) fkCid.prefix("a") is RCollective.Columns.id.prefix("b") -// else and(wh, fkCid.prefix("a") is RCollective.Columns.id.prefix("b"))) - def selectCount(col: Column, table: Fragment, where: Fragment): Fragment = Fragment.const("SELECT COUNT(") ++ col.f ++ Fragment.const(") FROM ") ++ table ++ this.where( where diff --git a/modules/store/src/main/scala/docspell/store/queries/QMails.scala b/modules/store/src/main/scala/docspell/store/queries/QMails.scala new file mode 100644 index 00000000..6053df10 --- /dev/null +++ b/modules/store/src/main/scala/docspell/store/queries/QMails.scala @@ -0,0 +1,64 @@ +package docspell.store.queries + +import cats.data.OptionT +import doobie._ +import doobie.implicits._ + +import docspell.common._ +import docspell.store.impl.Column +import docspell.store.impl.Implicits._ +import docspell.store.records.{RItem, RSentMail, RSentMailItem, RUser} + +object QMails { + + def delete(coll: Ident, mailId: Ident): ConnectionIO[Int] = + (for { + m <- OptionT(findMail(coll, mailId)) + k <- OptionT.liftF(RSentMailItem.deleteMail(mailId)) + n <- OptionT.liftF(RSentMail.delete(m._1.id)) + } yield k + n).getOrElse(0) + + def findMail(coll: Ident, mailId: Ident): ConnectionIO[Option[(RSentMail, Ident)]] = { + val iColl = RItem.Columns.cid.prefix("i") + val mId = RSentMail.Columns.id.prefix("m") + + val (cols, from) = partialFind + + val cond = Seq(mId.is(mailId), iColl.is(coll)) + + selectSimple(cols, from, and(cond)).query[(RSentMail, Ident)].option + } + + def findMails(coll: Ident, itemId: Ident): ConnectionIO[Vector[(RSentMail, Ident)]] = { + val iColl = RItem.Columns.cid.prefix("i") + val tItem = RSentMailItem.Columns.itemId.prefix("t") + val mCreated = RSentMail.Columns.created.prefix("m") + + val (cols, from) = partialFind + + val cond = Seq(tItem.is(itemId), iColl.is(coll)) + + (selectSimple(cols, from, and(cond)) ++ orderBy(mCreated.f) ++ fr"DESC") + .query[(RSentMail, Ident)] + .to[Vector] + } + + private def partialFind: (Seq[Column], Fragment) = { + val iId = RItem.Columns.id.prefix("i") + val tItem = RSentMailItem.Columns.itemId.prefix("t") + val tMail = RSentMailItem.Columns.sentMailId.prefix("t") + val mId = RSentMail.Columns.id.prefix("m") + val mUser = RSentMail.Columns.uid.prefix("m") + val uId = RUser.Columns.uid.prefix("u") + val uLogin = RUser.Columns.login.prefix("u") + + val cols = RSentMail.Columns.all.map(_.prefix("m")) :+ uLogin + val from = RSentMail.table ++ fr"m INNER JOIN" ++ + RSentMailItem.table ++ fr"t ON" ++ tMail.is(mId) ++ + fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ tItem.is(iId) ++ + fr"INNER JOIN" ++ RUser.table ++ fr"u ON" ++ uId.is(mUser) + + (cols, from) + } + +} diff --git a/modules/store/src/main/scala/docspell/store/records/RSentMail.scala b/modules/store/src/main/scala/docspell/store/records/RSentMail.scala index 8eb1b43d..a0679b20 100644 --- a/modules/store/src/main/scala/docspell/store/records/RSentMail.scala +++ b/modules/store/src/main/scala/docspell/store/records/RSentMail.scala @@ -16,6 +16,7 @@ case class RSentMail( uid: Ident, messageId: String, sender: MailAddress, + connName: Ident, subject: String, recipients: List[MailAddress], body: String, @@ -28,6 +29,7 @@ object RSentMail { uid: Ident, messageId: String, sender: MailAddress, + connName: Ident, subject: String, recipients: List[MailAddress], body: String @@ -35,24 +37,26 @@ object RSentMail { for { id <- Ident.randomId[F] now <- Timestamp.current[F] - } yield RSentMail(id, uid, messageId, sender, subject, recipients, body, now) + } yield RSentMail(id, uid, messageId, sender, connName, subject, recipients, body, now) def forItem( itemId: Ident, accId: AccountId, messageId: String, sender: MailAddress, + connName: Ident, subject: String, recipients: List[MailAddress], body: String ): OptionT[ConnectionIO, (RSentMail, RSentMailItem)] = for { user <- OptionT(RUser.findByAccount(accId)) - sm <- OptionT.liftF(RSentMail[ConnectionIO](user.uid, messageId, sender, subject, recipients, body)) + sm <- OptionT.liftF( + RSentMail[ConnectionIO](user.uid, messageId, sender, connName, subject, recipients, body) + ) si <- OptionT.liftF(RSentMailItem[ConnectionIO](itemId, sm.id, Some(sm.created))) } yield (sm, si) - val table = fr"sentmail" object Columns { @@ -60,6 +64,7 @@ object RSentMail { val uid = Column("uid") val messageId = Column("message_id") val sender = Column("sender") + val connName = Column("conn_name") val subject = Column("subject") val recipients = Column("recipients") val body = Column("body") @@ -70,6 +75,7 @@ object RSentMail { uid, messageId, sender, + connName, subject, recipients, body, @@ -83,10 +89,12 @@ object RSentMail { insertRow( table, all, - sql"${v.id},${v.uid},${v.messageId},${v.sender},${v.subject},${v.recipients},${v.body},${v.created}" + sql"${v.id},${v.uid},${v.messageId},${v.sender},${v.connName},${v.subject},${v.recipients},${v.body},${v.created}" ).update.run def findByUser(userId: Ident): Stream[ConnectionIO, RSentMail] = selectSimple(all, table, uid.is(userId)).query[RSentMail].stream + def delete(mailId: Ident): ConnectionIO[Int] = + deleteFrom(table, id.is(mailId)).update.run } diff --git a/modules/store/src/main/scala/docspell/store/records/RSentMailItem.scala b/modules/store/src/main/scala/docspell/store/records/RSentMailItem.scala index b9b12da2..5f796c6e 100644 --- a/modules/store/src/main/scala/docspell/store/records/RSentMailItem.scala +++ b/modules/store/src/main/scala/docspell/store/records/RSentMailItem.scala @@ -52,4 +52,6 @@ object RSentMailItem { sql"${v.id},${v.itemId},${v.sentMailId},${v.created}" ).update.run + def deleteMail(mailId: Ident): ConnectionIO[Int] = + deleteFrom(table, sentMailId.is(mailId)).update.run }