diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala
index da8958cc..59982761 100644
--- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala
+++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala
@@ -9,6 +9,7 @@ import docspell.store.ops.ONode
import docspell.store.queue.JobQueue
import scala.concurrent.ExecutionContext
+import emil.javamail.JavaMailEmil
trait BackendApp[F[_]] {
@@ -28,10 +29,11 @@ trait BackendApp[F[_]] {
object BackendApp {
- def create[F[_]: ConcurrentEffect](
+ def create[F[_]: ConcurrentEffect: ContextShift](
cfg: Config,
store: Store[F],
- httpClientEc: ExecutionContext
+ httpClientEc: ExecutionContext,
+ blocker: Blocker
): Resource[F, BackendApp[F]] =
for {
queue <- JobQueue(store)
@@ -46,7 +48,7 @@ object BackendApp {
nodeImpl <- ONode(store)
jobImpl <- OJob(store, httpClientEc)
itemImpl <- OItem(store)
- mailImpl <- OMail(store)
+ mailImpl <- OMail(store, JavaMailEmil(blocker))
} yield new BackendApp[F] {
val login: Login[F] = loginImpl
val signup: OSignup[F] = signupImpl
@@ -70,6 +72,6 @@ object BackendApp {
): Resource[F, BackendApp[F]] =
for {
store <- Store.create(cfg.jdbc, connectEC, blocker)
- backend <- create(cfg, store, httpClientEc)
+ backend <- create(cfg, store, httpClientEc, blocker)
} yield backend
}
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 6fd9ab32..80875f92 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala
@@ -1,14 +1,22 @@
package docspell.backend.ops
+import fs2.Stream
import cats.effect._
import cats.implicits._
import cats.data.OptionT
-import emil.{MailAddress, SSLType}
+import emil._
+import emil.javamail.syntax._
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
trait OMail[F[_]] {
@@ -35,10 +43,19 @@ object OMail {
attach: AttachSelection
)
- sealed trait AttachSelection
+ sealed trait AttachSelection {
+ def filter(v: Vector[(RAttachment, FileMeta)]): Vector[(RAttachment, FileMeta)]
+ }
object AttachSelection {
- case object All extends AttachSelection
- case class Selected(ids: List[Ident]) extends AttachSelection
+ case object All 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(
@@ -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] {
def getSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]] =
store.transact(RUserEmail.findByAccount(accId, nameQ))
@@ -97,7 +114,71 @@ object OMail {
def deleteSettings(accId: AccountId, name: Ident): F[Int] =
store.transact(RUserEmail.delete(accId, name))
- def sendMail(accId: AccountId, name: Ident, m: ItemMail): F[SendResult] =
- Effect[F].pure(SendResult.Failure(new Exception("not implemented")))
+ def sendMail(accId: AccountId, name: Ident, m: ItemMail): F[SendResult] = {
+
+ 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)
+ }
+
})
}
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/SendResult.scala b/modules/backend/src/main/scala/docspell/backend/ops/SendResult.scala
index b3ac6450..f64f48f6 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/SendResult.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/SendResult.scala
@@ -6,7 +6,21 @@ sealed trait SendResult
object SendResult {
+ /** Mail was successfully sent and stored to db.
+ */
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
}
diff --git a/modules/restserver/src/main/resources/logback.xml b/modules/restserver/src/main/resources/logback.xml
index c33ec1f7..f9b2d921 100644
--- a/modules/restserver/src/main/resources/logback.xml
+++ b/modules/restserver/src/main/resources/logback.xml
@@ -8,6 +8,8 @@
+
+
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala
index 8c10c33e..3d7a08e3 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala
@@ -46,7 +46,11 @@ object MailSendRoutes {
res match {
case SendResult.Success(_) =>
BasicResult(true, "Mail sent.")
- case SendResult.Failure(ex) =>
+ case SendResult.SendFailure(ex) =>
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.")
}
}
diff --git a/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala b/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala
index 84762e0b..62f058cd 100644
--- a/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala
+++ b/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala
@@ -95,7 +95,9 @@ trait DoobieMeta {
Meta[String].imap(EmilUtil.unsafeReadMailAddress)(EmilUtil.mailAddressString)
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 {
diff --git a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala
index 52f71d30..ee193e69 100644
--- a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala
@@ -64,6 +64,26 @@ object RAttachment {
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)]] = {
import bitpeace.sql._
diff --git a/modules/store/src/main/scala/docspell/store/records/RFileMeta.scala b/modules/store/src/main/scala/docspell/store/records/RFileMeta.scala
new file mode 100644
index 00000000..e6f206e5
--- /dev/null
+++ b/modules/store/src/main/scala/docspell/store/records/RFileMeta.scala
@@ -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)
+
+ }
+}
diff --git a/modules/store/src/main/scala/docspell/store/records/RItem.scala b/modules/store/src/main/scala/docspell/store/records/RItem.scala
index 7d9dafda..bf447317 100644
--- a/modules/store/src/main/scala/docspell/store/records/RItem.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RItem.scala
@@ -262,4 +262,7 @@ object RItem {
def deleteByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Int] =
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)
}
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 9bde3fcc..8eb1b43d 100644
--- a/modules/store/src/main/scala/docspell/store/records/RSentMail.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RSentMail.scala
@@ -1,12 +1,15 @@
package docspell.store.records
import fs2.Stream
+import cats.effect._
+import cats.implicits._
import doobie._
import doobie.implicits._
import docspell.common._
import docspell.store.impl.Column
import docspell.store.impl.Implicits._
import emil.MailAddress
+import cats.data.OptionT
case class RSentMail(
id: Ident,
@@ -21,17 +24,46 @@ case class 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"
object Columns {
- val id = Column("id")
- val uid = Column("uid")
- val messageId = Column("message_id")
- val sender = Column("sender")
- val subject = Column("subject")
- val recipients = Column("recipients")
+ val id = Column("id")
+ val uid = Column("uid")
+ val messageId = Column("message_id")
+ val sender = Column("sender")
+ val subject = Column("subject")
+ val recipients = Column("recipients")
val body = Column("body")
- val created = Column("created")
+ val created = Column("created")
val all = List(
id,
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 2a729539..b9b12da2 100644
--- a/modules/store/src/main/scala/docspell/store/records/RSentMailItem.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RSentMailItem.scala
@@ -17,13 +17,16 @@ case class 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 {
- id <- Ident.randomId[F]
+ id <- Ident.randomId[F]
now <- created.map(_.pure[F]).getOrElse(Timestamp.current[F])
} yield RSentMailItem(id, itemId, sentmailId, now)
-
val table = fr"sentmailitem"
object Columns {
diff --git a/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala b/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala
index cc67029b..e8fcc0b7 100644
--- a/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala
@@ -8,7 +8,7 @@ import cats.data.OptionT
import docspell.common._
import docspell.store.impl.Column
import docspell.store.impl.Implicits._
-import emil.{MailAddress, SSLType}
+import emil.{MailAddress, MailConfig, SSLType}
case class RUserEmail(
id: Ident,
@@ -23,7 +23,19 @@ case class RUserEmail(
mailFrom: MailAddress,
mailReplyTo: Option[MailAddress],
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 {
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 33dcd904..73bb2890 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -9,7 +9,7 @@ object Dependencies {
val BitpeaceVersion = "0.4.2"
val CirceVersion = "0.12.3"
val DoobieVersion = "0.8.8"
- val EmilVersion = "0.1.1"
+ val EmilVersion = "0.1.2-SNAPSHOT"
val FastparseVersion = "2.1.3"
val FlywayVersion = "6.1.3"
val Fs2Version = "2.1.0"