Add routes to retrieve sent mails

This commit is contained in:
Eike Kettner 2020-01-10 23:41:03 +01:00
parent b795a22992
commit 2ecfb679d9
9 changed files with 297 additions and 24 deletions

View File

@ -6,17 +6,13 @@ import cats.implicits._
import cats.data.OptionT import cats.data.OptionT
import emil._ import emil._
import emil.javamail.syntax._ import emil.javamail.syntax._
import bitpeace.{FileMeta, RangeDef}
import docspell.common._ import docspell.common._
import docspell.store._ import docspell.store._
import docspell.store.records.RUserEmail import docspell.store.records._
import OMail.{ItemMail, SmtpSettings} import docspell.store.queries.QMails
import docspell.store.records.RAttachment import OMail.{ItemMail, Sent, SmtpSettings}
import bitpeace.FileMeta
import bitpeace.RangeDef
import docspell.store.records.RItem
import docspell.store.records.RSentMail
import docspell.store.records.RSentMailItem
trait OMail[F[_]] { trait OMail[F[_]] {
@ -31,10 +27,32 @@ trait OMail[F[_]] {
def deleteSettings(accId: AccountId, name: Ident): F[Int] def deleteSettings(accId: AccountId, name: Ident): F[Int]
def sendMail(accId: AccountId, name: Ident, m: ItemMail): F[SendResult] 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 { 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( case class ItemMail(
item: Ident, item: Ident,
subject: String, subject: String,
@ -155,6 +173,7 @@ object OMail {
accId, accId,
msgId, msgId,
cfg.mailFrom, cfg.mailFrom,
name,
m.subject, m.subject,
m.recipients, m.recipients,
m.body m.body
@ -180,5 +199,17 @@ object OMail {
} yield conv).getOrElse(SendResult.NotFound) } 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))
}) })
} }

View File

@ -125,6 +125,8 @@ paths:
The result shows all items that contains a file with the given The result shows all items that contains a file with the given
checksum. checksum.
security:
- authTokenHeader: []
parameters: parameters:
- $ref: "#/components/parameters/checksum" - $ref: "#/components/parameters/checksum"
responses: responses:
@ -159,6 +161,8 @@ paths:
* application/pdf * application/pdf
Support for more types might be added. Support for more types might be added.
security:
- authTokenHeader: []
requestBody: requestBody:
content: content:
multipart/form-data: multipart/form-data:
@ -1188,6 +1192,8 @@ paths:
Get the current state of the job qeue. The job qeue contains Get the current state of the job qeue. The job qeue contains
all processing tasks and other long-running operations. All all processing tasks and other long-running operations. All
users/collectives share processing resources. users/collectives share processing resources.
security:
- authTokenHeader: []
responses: responses:
200: 200:
description: Ok description: Ok
@ -1203,6 +1209,8 @@ paths:
Tries to cancel a job and remove it from the queue. If the job 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 is running, a cancel request is send to the corresponding joex
instance. Otherwise the job is removed from the queue. instance. Otherwise the job is removed from the queue.
security:
- authTokenHeader: []
parameters: parameters:
- $ref: "#/components/parameters/id" - $ref: "#/components/parameters/id"
responses: responses:
@ -1224,6 +1232,8 @@ paths:
Multiple e-mail settings can be specified, they are Multiple e-mail settings can be specified, they are
distinguished by their `name`. The query `q` parameter does a distinguished by their `name`. The query `q` parameter does a
simple substring search in the connection name. simple substring search in the connection name.
security:
- authTokenHeader: []
parameters: parameters:
- $ref: "#/components/parameters/q" - $ref: "#/components/parameters/q"
responses: responses:
@ -1238,6 +1248,8 @@ paths:
summary: Create new email settings summary: Create new email settings
description: | description: |
Create new e-mail settings. Create new e-mail settings.
security:
- authTokenHeader: []
requestBody: requestBody:
content: content:
application/json: application/json:
@ -1259,6 +1271,8 @@ paths:
description: | description: |
Return the stored e-mail settings for the given connection Return the stored e-mail settings for the given connection
name. name.
security:
- authTokenHeader: []
responses: responses:
200: 200:
description: Ok description: Ok
@ -1271,6 +1285,8 @@ paths:
summary: Change specific email settings. summary: Change specific email settings.
description: | description: |
Changes all settings for the connection with the given `name`. Changes all settings for the connection with the given `name`.
security:
- authTokenHeader: []
requestBody: requestBody:
content: content:
application/json: application/json:
@ -1288,6 +1304,8 @@ paths:
summary: Delete e-mail settings. summary: Delete e-mail settings.
description: | description: |
Deletes the e-mail settings with the specified `name`. Deletes the e-mail settings with the specified `name`.
security:
- authTokenHeader: []
responses: responses:
200: 200:
description: Ok description: Ok
@ -1301,10 +1319,11 @@ paths:
tags: [ E-Mail ] tags: [ E-Mail ]
summary: Send an email. summary: Send an email.
description: | description: |
Sends an email as specified with all attachments of the item Sends an email as specified in the body of the request.
with `id` as mail attachments. If the item has no attachments,
then the mail is sent without any. If the item's attachments The item's attachment are added to the mail if requested.
exceed a specific size, the mail will not be sent. security:
- authTokenHeader: []
parameters: parameters:
- $ref: "#/components/parameters/name" - $ref: "#/components/parameters/name"
- $ref: "#/components/parameters/id" - $ref: "#/components/parameters/id"
@ -1320,9 +1339,100 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/BasicResult" $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: components:
schemas: 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: SimpleMail:
description: | description: |
A simple e-mail related to an item. A simple e-mail related to an item.
@ -1330,7 +1440,8 @@ components:
The mail may contain the item attachments as mail attachments. The mail may contain the item attachments as mail attachments.
If all item attachments should be send, set If all item attachments should be send, set
`addAllAttachments` to `true`. Otherwise set it to `false` and `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: required:
- recipients - recipients
- subject - subject
@ -2369,3 +2480,10 @@ components:
required: true required: true
schema: schema:
type: string type: string
mailId:
name: mailId
in: path
description: The id of a sent mail.
required: true
schema:
type: string

View File

@ -71,7 +71,8 @@ object RestServer {
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token), "upload" -> UploadRoutes.secured(restApp.backend, cfg, token),
"checkfile" -> CheckFileRoutes.secured(restApp.backend, token), "checkfile" -> CheckFileRoutes.secured(restApp.backend, token),
"email/send" -> MailSendRoutes(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] = def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =

View File

@ -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
)
}

View File

@ -20,6 +20,7 @@ CREATE TABLE "sentmail" (
"uid" varchar(254) not null, "uid" varchar(254) not null,
"message_id" varchar(254) not null, "message_id" varchar(254) not null,
"sender" varchar(254) not null, "sender" varchar(254) not null,
"conn_name" varchar(254) not null,
"subject" varchar(254) not null, "subject" varchar(254) not null,
"recipients" varchar(254) not null, "recipients" varchar(254) not null,
"body" text not null, "body" text not null,

View File

@ -65,12 +65,6 @@ trait DoobieSyntax {
Fragment.const("SELECT DISTINCT(") ++ commas(cols.map(_.f)) ++ Fragment.const("SELECT DISTINCT(") ++ commas(cols.map(_.f)) ++
Fragment.const(") FROM ") ++ table ++ this.where(where) 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 = def selectCount(col: Column, table: Fragment, where: Fragment): Fragment =
Fragment.const("SELECT COUNT(") ++ col.f ++ Fragment.const(") FROM ") ++ table ++ this.where( Fragment.const("SELECT COUNT(") ++ col.f ++ Fragment.const(") FROM ") ++ table ++ this.where(
where where

View File

@ -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)
}
}

View File

@ -16,6 +16,7 @@ case class RSentMail(
uid: Ident, uid: Ident,
messageId: String, messageId: String,
sender: MailAddress, sender: MailAddress,
connName: Ident,
subject: String, subject: String,
recipients: List[MailAddress], recipients: List[MailAddress],
body: String, body: String,
@ -28,6 +29,7 @@ object RSentMail {
uid: Ident, uid: Ident,
messageId: String, messageId: String,
sender: MailAddress, sender: MailAddress,
connName: Ident,
subject: String, subject: String,
recipients: List[MailAddress], recipients: List[MailAddress],
body: String body: String
@ -35,24 +37,26 @@ object RSentMail {
for { for {
id <- Ident.randomId[F] id <- Ident.randomId[F]
now <- Timestamp.current[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( def forItem(
itemId: Ident, itemId: Ident,
accId: AccountId, accId: AccountId,
messageId: String, messageId: String,
sender: MailAddress, sender: MailAddress,
connName: Ident,
subject: String, subject: String,
recipients: List[MailAddress], recipients: List[MailAddress],
body: String body: String
): OptionT[ConnectionIO, (RSentMail, RSentMailItem)] = ): OptionT[ConnectionIO, (RSentMail, RSentMailItem)] =
for { for {
user <- OptionT(RUser.findByAccount(accId)) 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))) si <- OptionT.liftF(RSentMailItem[ConnectionIO](itemId, sm.id, Some(sm.created)))
} yield (sm, si) } yield (sm, si)
val table = fr"sentmail" val table = fr"sentmail"
object Columns { object Columns {
@ -60,6 +64,7 @@ object RSentMail {
val uid = Column("uid") val uid = Column("uid")
val messageId = Column("message_id") val messageId = Column("message_id")
val sender = Column("sender") val sender = Column("sender")
val connName = Column("conn_name")
val subject = Column("subject") val subject = Column("subject")
val recipients = Column("recipients") val recipients = Column("recipients")
val body = Column("body") val body = Column("body")
@ -70,6 +75,7 @@ object RSentMail {
uid, uid,
messageId, messageId,
sender, sender,
connName,
subject, subject,
recipients, recipients,
body, body,
@ -83,10 +89,12 @@ object RSentMail {
insertRow( insertRow(
table, table,
all, 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 ).update.run
def findByUser(userId: Ident): Stream[ConnectionIO, RSentMail] = def findByUser(userId: Ident): Stream[ConnectionIO, RSentMail] =
selectSimple(all, table, uid.is(userId)).query[RSentMail].stream selectSimple(all, table, uid.is(userId)).query[RSentMail].stream
def delete(mailId: Ident): ConnectionIO[Int] =
deleteFrom(table, id.is(mailId)).update.run
} }

View File

@ -52,4 +52,6 @@ object RSentMailItem {
sql"${v.id},${v.itemId},${v.sentMailId},${v.created}" sql"${v.id},${v.itemId},${v.sentMailId},${v.created}"
).update.run ).update.run
def deleteMail(mailId: Ident): ConnectionIO[Int] =
deleteFrom(table, sentMailId.is(mailId)).update.run
} }