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

View File

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

View File

@ -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] =

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,
"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,

View File

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

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,
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
}

View File

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