mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-02 13:32:51 +00:00
Send mails for items
This commit is contained in:
parent
2d69d39dd1
commit
b795a22992
@ -9,6 +9,7 @@ import docspell.store.ops.ONode
|
|||||||
import docspell.store.queue.JobQueue
|
import docspell.store.queue.JobQueue
|
||||||
|
|
||||||
import scala.concurrent.ExecutionContext
|
import scala.concurrent.ExecutionContext
|
||||||
|
import emil.javamail.JavaMailEmil
|
||||||
|
|
||||||
trait BackendApp[F[_]] {
|
trait BackendApp[F[_]] {
|
||||||
|
|
||||||
@ -28,10 +29,11 @@ trait BackendApp[F[_]] {
|
|||||||
|
|
||||||
object BackendApp {
|
object BackendApp {
|
||||||
|
|
||||||
def create[F[_]: ConcurrentEffect](
|
def create[F[_]: ConcurrentEffect: ContextShift](
|
||||||
cfg: Config,
|
cfg: Config,
|
||||||
store: Store[F],
|
store: Store[F],
|
||||||
httpClientEc: ExecutionContext
|
httpClientEc: ExecutionContext,
|
||||||
|
blocker: Blocker
|
||||||
): Resource[F, BackendApp[F]] =
|
): Resource[F, BackendApp[F]] =
|
||||||
for {
|
for {
|
||||||
queue <- JobQueue(store)
|
queue <- JobQueue(store)
|
||||||
@ -46,7 +48,7 @@ object BackendApp {
|
|||||||
nodeImpl <- ONode(store)
|
nodeImpl <- ONode(store)
|
||||||
jobImpl <- OJob(store, httpClientEc)
|
jobImpl <- OJob(store, httpClientEc)
|
||||||
itemImpl <- OItem(store)
|
itemImpl <- OItem(store)
|
||||||
mailImpl <- OMail(store)
|
mailImpl <- OMail(store, JavaMailEmil(blocker))
|
||||||
} yield new BackendApp[F] {
|
} yield new BackendApp[F] {
|
||||||
val login: Login[F] = loginImpl
|
val login: Login[F] = loginImpl
|
||||||
val signup: OSignup[F] = signupImpl
|
val signup: OSignup[F] = signupImpl
|
||||||
@ -70,6 +72,6 @@ object BackendApp {
|
|||||||
): Resource[F, BackendApp[F]] =
|
): Resource[F, BackendApp[F]] =
|
||||||
for {
|
for {
|
||||||
store <- Store.create(cfg.jdbc, connectEC, blocker)
|
store <- Store.create(cfg.jdbc, connectEC, blocker)
|
||||||
backend <- create(cfg, store, httpClientEc)
|
backend <- create(cfg, store, httpClientEc, blocker)
|
||||||
} yield backend
|
} yield backend
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
package docspell.backend.ops
|
package docspell.backend.ops
|
||||||
|
|
||||||
|
import fs2.Stream
|
||||||
import cats.effect._
|
import cats.effect._
|
||||||
import cats.implicits._
|
import cats.implicits._
|
||||||
import cats.data.OptionT
|
import cats.data.OptionT
|
||||||
import emil.{MailAddress, SSLType}
|
import emil._
|
||||||
|
import emil.javamail.syntax._
|
||||||
|
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.store._
|
import docspell.store._
|
||||||
import docspell.store.records.RUserEmail
|
import docspell.store.records.RUserEmail
|
||||||
import OMail.{ItemMail, SmtpSettings}
|
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
|
||||||
|
|
||||||
trait OMail[F[_]] {
|
trait OMail[F[_]] {
|
||||||
|
|
||||||
@ -35,10 +43,19 @@ object OMail {
|
|||||||
attach: AttachSelection
|
attach: AttachSelection
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed trait AttachSelection
|
sealed trait AttachSelection {
|
||||||
|
def filter(v: Vector[(RAttachment, FileMeta)]): Vector[(RAttachment, FileMeta)]
|
||||||
|
}
|
||||||
object AttachSelection {
|
object AttachSelection {
|
||||||
case object All extends AttachSelection
|
case object All extends AttachSelection {
|
||||||
case class Selected(ids: List[Ident]) extends AttachSelection
|
def filter(v: Vector[(RAttachment, FileMeta)]): Vector[(RAttachment, FileMeta)] = v
|
||||||
|
}
|
||||||
|
case class Selected(ids: List[Ident]) extends AttachSelection {
|
||||||
|
def filter(v: Vector[(RAttachment, FileMeta)]): Vector[(RAttachment, FileMeta)] = {
|
||||||
|
val set = ids.toSet
|
||||||
|
v.filter(set contains _._1.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case class SmtpSettings(
|
case class SmtpSettings(
|
||||||
@ -68,7 +85,7 @@ object OMail {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply[F[_]: Effect](store: Store[F]): Resource[F, OMail[F]] =
|
def apply[F[_]: Effect](store: Store[F], emil: Emil[F]): Resource[F, OMail[F]] =
|
||||||
Resource.pure(new OMail[F] {
|
Resource.pure(new OMail[F] {
|
||||||
def getSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]] =
|
def getSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]] =
|
||||||
store.transact(RUserEmail.findByAccount(accId, nameQ))
|
store.transact(RUserEmail.findByAccount(accId, nameQ))
|
||||||
@ -97,7 +114,71 @@ object OMail {
|
|||||||
def deleteSettings(accId: AccountId, name: Ident): F[Int] =
|
def deleteSettings(accId: AccountId, name: Ident): F[Int] =
|
||||||
store.transact(RUserEmail.delete(accId, name))
|
store.transact(RUserEmail.delete(accId, name))
|
||||||
|
|
||||||
def sendMail(accId: AccountId, name: Ident, m: ItemMail): F[SendResult] =
|
def sendMail(accId: AccountId, name: Ident, m: ItemMail): F[SendResult] = {
|
||||||
Effect[F].pure(SendResult.Failure(new Exception("not implemented")))
|
|
||||||
|
val getSettings: OptionT[F, RUserEmail] =
|
||||||
|
OptionT(store.transact(RUserEmail.getByName(accId, name)))
|
||||||
|
|
||||||
|
def createMail(sett: RUserEmail): OptionT[F, Mail[F]] = {
|
||||||
|
import _root_.emil.builder._
|
||||||
|
|
||||||
|
for {
|
||||||
|
_ <- OptionT.liftF(store.transact(RItem.existsById(m.item))).filter(identity)
|
||||||
|
ras <- OptionT.liftF(
|
||||||
|
store.transact(RAttachment.findByItemAndCollectiveWithMeta(m.item, accId.collective))
|
||||||
|
)
|
||||||
|
} yield {
|
||||||
|
val addAttach = m.attach.filter(ras).map { a =>
|
||||||
|
Attach[F](Stream.emit(a._2).through(store.bitpeace.fetchData2(RangeDef.all)))
|
||||||
|
.withFilename(a._1.name)
|
||||||
|
.withLength(a._2.length)
|
||||||
|
.withMimeType(_root_.emil.MimeType.parse(a._2.mimetype.asString).toOption)
|
||||||
|
}
|
||||||
|
val fields: Seq[Trans[F]] = Seq(
|
||||||
|
From(sett.mailFrom),
|
||||||
|
Tos(m.recipients),
|
||||||
|
Subject(m.subject),
|
||||||
|
TextBody[F](m.body)
|
||||||
|
)
|
||||||
|
|
||||||
|
MailBuilder.fromSeq[F](fields).addAll(addAttach).build
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def sendMail(cfg: MailConfig, mail: Mail[F]): F[Either[SendResult, String]] =
|
||||||
|
emil(cfg).send(mail).map(_.head).attempt.map(_.left.map(SendResult.SendFailure))
|
||||||
|
|
||||||
|
def storeMail(msgId: String, cfg: RUserEmail): F[Either[SendResult, Ident]] = {
|
||||||
|
val save = for {
|
||||||
|
data <- RSentMail.forItem(
|
||||||
|
m.item,
|
||||||
|
accId,
|
||||||
|
msgId,
|
||||||
|
cfg.mailFrom,
|
||||||
|
m.subject,
|
||||||
|
m.recipients,
|
||||||
|
m.body
|
||||||
|
)
|
||||||
|
_ <- OptionT.liftF(RSentMail.insert(data._1))
|
||||||
|
_ <- OptionT.liftF(RSentMailItem.insert(data._2))
|
||||||
|
} yield data._1.id
|
||||||
|
|
||||||
|
store.transact(save.value).attempt.map {
|
||||||
|
case Right(Some(id)) => Right(id)
|
||||||
|
case Right(None) =>
|
||||||
|
Left(SendResult.StoreFailure(new Exception(s"Could not find user to save mail.")))
|
||||||
|
case Left(ex) => Left(SendResult.StoreFailure(ex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(for {
|
||||||
|
mailCfg <- getSettings
|
||||||
|
mail <- createMail(mailCfg)
|
||||||
|
mid <- OptionT.liftF(sendMail(mailCfg.toMailConfig, mail))
|
||||||
|
res <- mid.traverse(id => OptionT.liftF(storeMail(id, mailCfg)))
|
||||||
|
conv = res.fold(identity, _.fold(identity, id => SendResult.Success(id)))
|
||||||
|
} yield conv).getOrElse(SendResult.NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,21 @@ sealed trait SendResult
|
|||||||
|
|
||||||
object SendResult {
|
object SendResult {
|
||||||
|
|
||||||
|
/** Mail was successfully sent and stored to db.
|
||||||
|
*/
|
||||||
case class Success(id: Ident) extends SendResult
|
case class Success(id: Ident) extends SendResult
|
||||||
|
|
||||||
case class Failure(ex: Throwable) extends SendResult
|
/** There was a failure sending the mail. The mail is then not saved
|
||||||
|
* to db.
|
||||||
|
*/
|
||||||
|
case class SendFailure(ex: Throwable) extends SendResult
|
||||||
|
|
||||||
|
/** The mail was successfully sent, but storing to db failed.
|
||||||
|
*/
|
||||||
|
case class StoreFailure(ex: Throwable) extends SendResult
|
||||||
|
|
||||||
|
/** Something could not be found required for sending (mail configs,
|
||||||
|
* items etc).
|
||||||
|
*/
|
||||||
|
case object NotFound extends SendResult
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<logger name="docspell" level="debug" />
|
<logger name="docspell" level="debug" />
|
||||||
|
<logger name="emil" level="debug"/>
|
||||||
|
|
||||||
<root level="INFO">
|
<root level="INFO">
|
||||||
<appender-ref ref="STDOUT" />
|
<appender-ref ref="STDOUT" />
|
||||||
</root>
|
</root>
|
||||||
|
@ -46,7 +46,11 @@ object MailSendRoutes {
|
|||||||
res match {
|
res match {
|
||||||
case SendResult.Success(_) =>
|
case SendResult.Success(_) =>
|
||||||
BasicResult(true, "Mail sent.")
|
BasicResult(true, "Mail sent.")
|
||||||
case SendResult.Failure(ex) =>
|
case SendResult.SendFailure(ex) =>
|
||||||
BasicResult(false, s"Mail sending failed: ${ex.getMessage}")
|
BasicResult(false, s"Mail sending failed: ${ex.getMessage}")
|
||||||
|
case SendResult.StoreFailure(ex) =>
|
||||||
|
BasicResult(false, s"Mail was sent, but could not be store to database: ${ex.getMessage}")
|
||||||
|
case SendResult.NotFound =>
|
||||||
|
BasicResult(false, s"There was no mail-connection or item found.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,9 @@ trait DoobieMeta {
|
|||||||
Meta[String].imap(EmilUtil.unsafeReadMailAddress)(EmilUtil.mailAddressString)
|
Meta[String].imap(EmilUtil.unsafeReadMailAddress)(EmilUtil.mailAddressString)
|
||||||
|
|
||||||
implicit def mailAddressList: Meta[List[MailAddress]] =
|
implicit def mailAddressList: Meta[List[MailAddress]] =
|
||||||
???
|
Meta[String].imap(str => str.split(',').toList.map(_.trim).map(EmilUtil.unsafeReadMailAddress))(
|
||||||
|
lma => lma.map(EmilUtil.mailAddressString).mkString(",")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
object DoobieMeta extends DoobieMeta {
|
object DoobieMeta extends DoobieMeta {
|
||||||
|
@ -64,6 +64,26 @@ object RAttachment {
|
|||||||
q.query[RAttachment].to[Vector]
|
q.query[RAttachment].to[Vector]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def findByItemAndCollectiveWithMeta(
|
||||||
|
id: Ident,
|
||||||
|
coll: Ident
|
||||||
|
): ConnectionIO[Vector[(RAttachment, FileMeta)]] = {
|
||||||
|
import bitpeace.sql._
|
||||||
|
|
||||||
|
val cols = all.map(_.prefix("a")) ++ RFileMeta.Columns.all.map(_.prefix("m"))
|
||||||
|
val afileMeta = fileId.prefix("a")
|
||||||
|
val aItem = itemId.prefix("a")
|
||||||
|
val mId = RFileMeta.Columns.id.prefix("m")
|
||||||
|
val iId = RItem.Columns.id.prefix("i")
|
||||||
|
val iColl = RItem.Columns.cid.prefix("i")
|
||||||
|
|
||||||
|
val from = table ++ fr"a INNER JOIN" ++ RFileMeta.table ++ fr"m ON" ++ afileMeta.is(mId) ++
|
||||||
|
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ aItem.is(iId)
|
||||||
|
val cond = Seq(aItem.is(id), iColl.is(coll))
|
||||||
|
|
||||||
|
selectSimple(cols, from, and(cond)).query[(RAttachment, FileMeta)].to[Vector]
|
||||||
|
}
|
||||||
|
|
||||||
def findByItemWithMeta(id: Ident): ConnectionIO[Vector[(RAttachment, FileMeta)]] = {
|
def findByItemWithMeta(id: Ident): ConnectionIO[Vector[(RAttachment, FileMeta)]] = {
|
||||||
import bitpeace.sql._
|
import bitpeace.sql._
|
||||||
|
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package docspell.store.records
|
||||||
|
|
||||||
|
import doobie.implicits._
|
||||||
|
import docspell.store.impl._
|
||||||
|
|
||||||
|
object RFileMeta {
|
||||||
|
|
||||||
|
val table = fr"filemeta"
|
||||||
|
|
||||||
|
object Columns {
|
||||||
|
val id = Column("id")
|
||||||
|
val timestamp = Column("timestamp")
|
||||||
|
val mimetype = Column("mimetype")
|
||||||
|
val length = Column("length")
|
||||||
|
val checksum = Column("checksum")
|
||||||
|
val chunks = Column("chunks")
|
||||||
|
val chunksize = Column("chunksize")
|
||||||
|
|
||||||
|
val all = List(id, timestamp, mimetype, length, checksum, chunks, chunksize)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -262,4 +262,7 @@ object RItem {
|
|||||||
|
|
||||||
def deleteByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Int] =
|
def deleteByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Int] =
|
||||||
deleteFrom(table, and(id.is(itemId), cid.is(coll))).update.run
|
deleteFrom(table, and(id.is(itemId), cid.is(coll))).update.run
|
||||||
|
|
||||||
|
def existsById(itemId: Ident): ConnectionIO[Boolean] =
|
||||||
|
selectCount(id, table, id.is(itemId)).query[Int].unique.map(_ > 0)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
package docspell.store.records
|
package docspell.store.records
|
||||||
|
|
||||||
import fs2.Stream
|
import fs2.Stream
|
||||||
|
import cats.effect._
|
||||||
|
import cats.implicits._
|
||||||
import doobie._
|
import doobie._
|
||||||
import doobie.implicits._
|
import doobie.implicits._
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.store.impl.Column
|
import docspell.store.impl.Column
|
||||||
import docspell.store.impl.Implicits._
|
import docspell.store.impl.Implicits._
|
||||||
import emil.MailAddress
|
import emil.MailAddress
|
||||||
|
import cats.data.OptionT
|
||||||
|
|
||||||
case class RSentMail(
|
case class RSentMail(
|
||||||
id: Ident,
|
id: Ident,
|
||||||
@ -21,17 +24,46 @@ case class RSentMail(
|
|||||||
|
|
||||||
object RSentMail {
|
object RSentMail {
|
||||||
|
|
||||||
|
def apply[F[_]: Sync](
|
||||||
|
uid: Ident,
|
||||||
|
messageId: String,
|
||||||
|
sender: MailAddress,
|
||||||
|
subject: String,
|
||||||
|
recipients: List[MailAddress],
|
||||||
|
body: String
|
||||||
|
): F[RSentMail] =
|
||||||
|
for {
|
||||||
|
id <- Ident.randomId[F]
|
||||||
|
now <- Timestamp.current[F]
|
||||||
|
} yield RSentMail(id, uid, messageId, sender, subject, recipients, body, now)
|
||||||
|
|
||||||
|
def forItem(
|
||||||
|
itemId: Ident,
|
||||||
|
accId: AccountId,
|
||||||
|
messageId: String,
|
||||||
|
sender: MailAddress,
|
||||||
|
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))
|
||||||
|
si <- OptionT.liftF(RSentMailItem[ConnectionIO](itemId, sm.id, Some(sm.created)))
|
||||||
|
} yield (sm, si)
|
||||||
|
|
||||||
|
|
||||||
val table = fr"sentmail"
|
val table = fr"sentmail"
|
||||||
|
|
||||||
object Columns {
|
object Columns {
|
||||||
val id = Column("id")
|
val id = Column("id")
|
||||||
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 subject = Column("subject")
|
val subject = Column("subject")
|
||||||
val recipients = Column("recipients")
|
val recipients = Column("recipients")
|
||||||
val body = Column("body")
|
val body = Column("body")
|
||||||
val created = Column("created")
|
val created = Column("created")
|
||||||
|
|
||||||
val all = List(
|
val all = List(
|
||||||
id,
|
id,
|
||||||
|
@ -17,13 +17,16 @@ case class RSentMailItem(
|
|||||||
|
|
||||||
object RSentMailItem {
|
object RSentMailItem {
|
||||||
|
|
||||||
def create[F[_]: Sync](itemId: Ident, sentmailId: Ident, created: Option[Timestamp] = None): F[RSentMailItem] =
|
def apply[F[_]: Sync](
|
||||||
|
itemId: Ident,
|
||||||
|
sentmailId: Ident,
|
||||||
|
created: Option[Timestamp] = None
|
||||||
|
): F[RSentMailItem] =
|
||||||
for {
|
for {
|
||||||
id <- Ident.randomId[F]
|
id <- Ident.randomId[F]
|
||||||
now <- created.map(_.pure[F]).getOrElse(Timestamp.current[F])
|
now <- created.map(_.pure[F]).getOrElse(Timestamp.current[F])
|
||||||
} yield RSentMailItem(id, itemId, sentmailId, now)
|
} yield RSentMailItem(id, itemId, sentmailId, now)
|
||||||
|
|
||||||
|
|
||||||
val table = fr"sentmailitem"
|
val table = fr"sentmailitem"
|
||||||
|
|
||||||
object Columns {
|
object Columns {
|
||||||
|
@ -8,7 +8,7 @@ import cats.data.OptionT
|
|||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.store.impl.Column
|
import docspell.store.impl.Column
|
||||||
import docspell.store.impl.Implicits._
|
import docspell.store.impl.Implicits._
|
||||||
import emil.{MailAddress, SSLType}
|
import emil.{MailAddress, MailConfig, SSLType}
|
||||||
|
|
||||||
case class RUserEmail(
|
case class RUserEmail(
|
||||||
id: Ident,
|
id: Ident,
|
||||||
@ -23,7 +23,19 @@ case class RUserEmail(
|
|||||||
mailFrom: MailAddress,
|
mailFrom: MailAddress,
|
||||||
mailReplyTo: Option[MailAddress],
|
mailReplyTo: Option[MailAddress],
|
||||||
created: Timestamp
|
created: Timestamp
|
||||||
) {}
|
) {
|
||||||
|
|
||||||
|
def toMailConfig: MailConfig = {
|
||||||
|
val port = smtpPort.map(p => s":$p").getOrElse("")
|
||||||
|
MailConfig(
|
||||||
|
s"smtp://$smtpHost$port",
|
||||||
|
smtpUser.getOrElse(""),
|
||||||
|
smtpPassword.map(_.pass).getOrElse(""),
|
||||||
|
smtpSsl,
|
||||||
|
!smtpCertCheck
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
object RUserEmail {
|
object RUserEmail {
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ object Dependencies {
|
|||||||
val BitpeaceVersion = "0.4.2"
|
val BitpeaceVersion = "0.4.2"
|
||||||
val CirceVersion = "0.12.3"
|
val CirceVersion = "0.12.3"
|
||||||
val DoobieVersion = "0.8.8"
|
val DoobieVersion = "0.8.8"
|
||||||
val EmilVersion = "0.1.1"
|
val EmilVersion = "0.1.2-SNAPSHOT"
|
||||||
val FastparseVersion = "2.1.3"
|
val FastparseVersion = "2.1.3"
|
||||||
val FlywayVersion = "6.1.3"
|
val FlywayVersion = "6.1.3"
|
||||||
val Fs2Version = "2.1.0"
|
val Fs2Version = "2.1.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user