mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-11-04 12:30:12 +00:00 
			
		
		
		
	@@ -148,7 +148,8 @@ val store = project.in(file("modules/store")).
 | 
			
		||||
      Dependencies.fs2 ++
 | 
			
		||||
      Dependencies.databases ++
 | 
			
		||||
      Dependencies.flyway ++
 | 
			
		||||
      Dependencies.loggingApi
 | 
			
		||||
      Dependencies.loggingApi ++
 | 
			
		||||
      Dependencies.emil
 | 
			
		||||
  ).dependsOn(common)
 | 
			
		||||
 | 
			
		||||
val text = project.in(file("modules/text")).
 | 
			
		||||
@@ -225,7 +226,8 @@ val backend = project.in(file("modules/backend")).
 | 
			
		||||
      Dependencies.loggingApi ++
 | 
			
		||||
      Dependencies.fs2 ++
 | 
			
		||||
      Dependencies.bcrypt ++
 | 
			
		||||
      Dependencies.http4sClient
 | 
			
		||||
      Dependencies.http4sClient ++
 | 
			
		||||
      Dependencies.emil
 | 
			
		||||
  ).dependsOn(store)
 | 
			
		||||
 | 
			
		||||
val webapp = project.in(file("modules/webapp")).
 | 
			
		||||
 
 | 
			
		||||
@@ -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[_]] {
 | 
			
		||||
 | 
			
		||||
@@ -23,14 +24,16 @@ trait BackendApp[F[_]] {
 | 
			
		||||
  def node: ONode[F]
 | 
			
		||||
  def job: OJob[F]
 | 
			
		||||
  def item: OItem[F]
 | 
			
		||||
  def mail: OMail[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)
 | 
			
		||||
@@ -45,6 +48,7 @@ object BackendApp {
 | 
			
		||||
      nodeImpl   <- ONode(store)
 | 
			
		||||
      jobImpl    <- OJob(store, httpClientEc)
 | 
			
		||||
      itemImpl   <- OItem(store)
 | 
			
		||||
      mailImpl   <- OMail(store, JavaMailEmil(blocker))
 | 
			
		||||
    } yield new BackendApp[F] {
 | 
			
		||||
      val login: Login[F]            = loginImpl
 | 
			
		||||
      val signup: OSignup[F]         = signupImpl
 | 
			
		||||
@@ -57,6 +61,7 @@ object BackendApp {
 | 
			
		||||
      val node                       = nodeImpl
 | 
			
		||||
      val job                        = jobImpl
 | 
			
		||||
      val item                       = itemImpl
 | 
			
		||||
      val mail                       = mailImpl
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  def apply[F[_]: ConcurrentEffect: ContextShift](
 | 
			
		||||
@@ -67,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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										215
									
								
								modules/backend/src/main/scala/docspell/backend/ops/OMail.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								modules/backend/src/main/scala/docspell/backend/ops/OMail.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,215 @@
 | 
			
		||||
package docspell.backend.ops
 | 
			
		||||
 | 
			
		||||
import fs2.Stream
 | 
			
		||||
import cats.effect._
 | 
			
		||||
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._
 | 
			
		||||
import docspell.store.queries.QMails
 | 
			
		||||
import OMail.{ItemMail, Sent, SmtpSettings}
 | 
			
		||||
 | 
			
		||||
trait OMail[F[_]] {
 | 
			
		||||
 | 
			
		||||
  def getSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]]
 | 
			
		||||
 | 
			
		||||
  def findSettings(accId: AccountId, name: Ident): OptionT[F, RUserEmail]
 | 
			
		||||
 | 
			
		||||
  def createSettings(accId: AccountId, data: SmtpSettings): F[AddResult]
 | 
			
		||||
 | 
			
		||||
  def updateSettings(accId: AccountId, name: Ident, data: OMail.SmtpSettings): F[Int]
 | 
			
		||||
 | 
			
		||||
  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,
 | 
			
		||||
      recipients: List[MailAddress],
 | 
			
		||||
      body: String,
 | 
			
		||||
      attach: AttachSelection
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  sealed trait AttachSelection {
 | 
			
		||||
    def filter(v: Vector[(RAttachment, FileMeta)]): Vector[(RAttachment, FileMeta)]
 | 
			
		||||
  }
 | 
			
		||||
  object 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(
 | 
			
		||||
      name: Ident,
 | 
			
		||||
      smtpHost: String,
 | 
			
		||||
      smtpPort: Option[Int],
 | 
			
		||||
      smtpUser: Option[String],
 | 
			
		||||
      smtpPassword: Option[Password],
 | 
			
		||||
      smtpSsl: SSLType,
 | 
			
		||||
      smtpCertCheck: Boolean,
 | 
			
		||||
      mailFrom: MailAddress,
 | 
			
		||||
      mailReplyTo: Option[MailAddress]
 | 
			
		||||
  ) {
 | 
			
		||||
 | 
			
		||||
    def toRecord(accId: AccountId) =
 | 
			
		||||
      RUserEmail.fromAccount(
 | 
			
		||||
        accId,
 | 
			
		||||
        name,
 | 
			
		||||
        smtpHost,
 | 
			
		||||
        smtpPort,
 | 
			
		||||
        smtpUser,
 | 
			
		||||
        smtpPassword,
 | 
			
		||||
        smtpSsl,
 | 
			
		||||
        smtpCertCheck,
 | 
			
		||||
        mailFrom,
 | 
			
		||||
        mailReplyTo
 | 
			
		||||
      )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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))
 | 
			
		||||
 | 
			
		||||
      def findSettings(accId: AccountId, name: Ident): OptionT[F, RUserEmail] =
 | 
			
		||||
        OptionT(store.transact(RUserEmail.getByName(accId, name)))
 | 
			
		||||
 | 
			
		||||
      def createSettings(accId: AccountId, s: SmtpSettings): F[AddResult] =
 | 
			
		||||
        (for {
 | 
			
		||||
          ru <- OptionT(store.transact(s.toRecord(accId).value))
 | 
			
		||||
          ins    = RUserEmail.insert(ru)
 | 
			
		||||
          exists = RUserEmail.exists(ru.uid, ru.name)
 | 
			
		||||
          res <- OptionT.liftF(store.add(ins, exists))
 | 
			
		||||
        } yield res).getOrElse(AddResult.Failure(new Exception("User not found")))
 | 
			
		||||
 | 
			
		||||
      def updateSettings(accId: AccountId, name: Ident, data: SmtpSettings): F[Int] = {
 | 
			
		||||
        val op = for {
 | 
			
		||||
          um <- OptionT(RUserEmail.getByName(accId, name))
 | 
			
		||||
          ru <- data.toRecord(accId)
 | 
			
		||||
          n  <- OptionT.liftF(RUserEmail.update(um.id, ru))
 | 
			
		||||
        } yield n
 | 
			
		||||
 | 
			
		||||
        store.transact(op.value).map(_.getOrElse(0))
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      def deleteSettings(accId: AccountId, name: Ident): F[Int] =
 | 
			
		||||
        store.transact(RUserEmail.delete(accId, name))
 | 
			
		||||
 | 
			
		||||
      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,
 | 
			
		||||
              name,
 | 
			
		||||
              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)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      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))
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
package docspell.backend.ops
 | 
			
		||||
 | 
			
		||||
import docspell.common._
 | 
			
		||||
 | 
			
		||||
sealed trait SendResult
 | 
			
		||||
 | 
			
		||||
object SendResult {
 | 
			
		||||
 | 
			
		||||
  /** Mail was successfully sent and stored to db.
 | 
			
		||||
    */
 | 
			
		||||
  case class Success(id: Ident) 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
 | 
			
		||||
}
 | 
			
		||||
@@ -15,7 +15,7 @@ object Ident {
 | 
			
		||||
  implicit val identEq: Eq[Ident] =
 | 
			
		||||
    Eq.by(_.id)
 | 
			
		||||
 | 
			
		||||
  val chars: Set[Char] = (('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "-_").toSet
 | 
			
		||||
  val chars: Set[Char] = (('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "-_.").toSet
 | 
			
		||||
 | 
			
		||||
  def randomUUID[F[_]: Sync]: F[Ident] =
 | 
			
		||||
    Sync[F].delay(unsafe(UUID.randomUUID.toString))
 | 
			
		||||
@@ -32,7 +32,7 @@ object Ident {
 | 
			
		||||
 | 
			
		||||
  def fromString(s: String): Either[String, Ident] =
 | 
			
		||||
    if (s.forall(chars.contains)) Right(new Ident(s))
 | 
			
		||||
    else Left(s"Invalid identifier: $s. Allowed chars: ${chars.mkString}")
 | 
			
		||||
    else Left(s"Invalid identifier: '$s'. Allowed chars: ${chars.toList.sorted.mkString}")
 | 
			
		||||
 | 
			
		||||
  def fromBytes(bytes: ByteVector): Ident =
 | 
			
		||||
    unsafe(bytes.toBase58)
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
@@ -1212,8 +1220,291 @@ paths:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: "#/components/schemas/BasicResult"
 | 
			
		||||
  /sec/email/settings:
 | 
			
		||||
    get:
 | 
			
		||||
      tags: [ E-Mail ]
 | 
			
		||||
      summary: List email settings for current user.
 | 
			
		||||
      description: |
 | 
			
		||||
        List all available e-mail settings for the current user.
 | 
			
		||||
        E-Mail settings specify smtp connections that can be used to
 | 
			
		||||
        sent e-mails.
 | 
			
		||||
 | 
			
		||||
        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:
 | 
			
		||||
        200:
 | 
			
		||||
          description: Ok
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: "#/components/schemas/EmailSettingsList"
 | 
			
		||||
    post:
 | 
			
		||||
      tags: [ E-Mail ]
 | 
			
		||||
      summary: Create new email settings
 | 
			
		||||
      description: |
 | 
			
		||||
        Create new e-mail settings.
 | 
			
		||||
      security:
 | 
			
		||||
        - authTokenHeader: []
 | 
			
		||||
      requestBody:
 | 
			
		||||
        content:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              $ref: "#/components/schemas/EmailSettings"
 | 
			
		||||
      responses:
 | 
			
		||||
        200:
 | 
			
		||||
          description: Ok
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: "#/components/schemas/BasicResult"
 | 
			
		||||
  /sec/email/settings/{name}:
 | 
			
		||||
    parameters:
 | 
			
		||||
      - $ref: "#/components/parameters/name"
 | 
			
		||||
    get:
 | 
			
		||||
      tags: [ E-Mail ]
 | 
			
		||||
      summary: Return specific email settings by name.
 | 
			
		||||
      description: |
 | 
			
		||||
        Return the stored e-mail settings for the given connection
 | 
			
		||||
        name.
 | 
			
		||||
      security:
 | 
			
		||||
        - authTokenHeader: []
 | 
			
		||||
      responses:
 | 
			
		||||
        200:
 | 
			
		||||
          description: Ok
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: "#/components/schemas/EmailSettings"
 | 
			
		||||
    put:
 | 
			
		||||
      tags: [ E-Mail ]
 | 
			
		||||
      summary: Change specific email settings.
 | 
			
		||||
      description: |
 | 
			
		||||
        Changes all settings for the connection with the given `name`.
 | 
			
		||||
      security:
 | 
			
		||||
        - authTokenHeader: []
 | 
			
		||||
      requestBody:
 | 
			
		||||
        content:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              $ref: "#/components/schemas/EmailSettings"
 | 
			
		||||
      responses:
 | 
			
		||||
        200:
 | 
			
		||||
          description: Ok
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: "#/components/schemas/BasicResult"
 | 
			
		||||
    delete:
 | 
			
		||||
      tags: [ E-Mail ]
 | 
			
		||||
      summary: Delete e-mail settings.
 | 
			
		||||
      description: |
 | 
			
		||||
        Deletes the e-mail settings with the specified `name`.
 | 
			
		||||
      security:
 | 
			
		||||
        - authTokenHeader: []
 | 
			
		||||
      responses:
 | 
			
		||||
        200:
 | 
			
		||||
          description: Ok
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: "#/components/schemas/BasicResult"
 | 
			
		||||
 | 
			
		||||
  /sec/email/send/{name}/{id}:
 | 
			
		||||
    post:
 | 
			
		||||
      tags: [ E-Mail ]
 | 
			
		||||
      summary: Send an email.
 | 
			
		||||
      description: |
 | 
			
		||||
        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"
 | 
			
		||||
      requestBody:
 | 
			
		||||
        content:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              $ref: "#/components/schemas/SimpleMail"
 | 
			
		||||
      responses:
 | 
			
		||||
        200:
 | 
			
		||||
          description: Ok
 | 
			
		||||
          content:
 | 
			
		||||
            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.
 | 
			
		||||
 | 
			
		||||
        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. This list
 | 
			
		||||
        is ignored, if `addAllAttachments` is set to `true`.
 | 
			
		||||
      required:
 | 
			
		||||
        - recipients
 | 
			
		||||
        - subject
 | 
			
		||||
        - body
 | 
			
		||||
        - addAllAttachments
 | 
			
		||||
        - attachmentIds
 | 
			
		||||
      properties:
 | 
			
		||||
        recipients:
 | 
			
		||||
          type: array
 | 
			
		||||
          items:
 | 
			
		||||
            type: string
 | 
			
		||||
        subject:
 | 
			
		||||
          type: string
 | 
			
		||||
        body:
 | 
			
		||||
          type: string
 | 
			
		||||
        addAllAttachments:
 | 
			
		||||
          type: boolean
 | 
			
		||||
        attachmentIds:
 | 
			
		||||
          type: array
 | 
			
		||||
          items:
 | 
			
		||||
            type: string
 | 
			
		||||
            format: ident
 | 
			
		||||
    EmailSettingsList:
 | 
			
		||||
      description: |
 | 
			
		||||
        A list of user email settings.
 | 
			
		||||
      required:
 | 
			
		||||
        - items
 | 
			
		||||
      properties:
 | 
			
		||||
        items:
 | 
			
		||||
          type: array
 | 
			
		||||
          items:
 | 
			
		||||
            $ref: "#/components/schemas/EmailSettings"
 | 
			
		||||
    EmailSettings:
 | 
			
		||||
      description: |
 | 
			
		||||
        SMTP settings for sending mail.
 | 
			
		||||
      required:
 | 
			
		||||
        - name
 | 
			
		||||
        - smtpHost
 | 
			
		||||
        - from
 | 
			
		||||
        - sslType
 | 
			
		||||
        - ignoreCertificates
 | 
			
		||||
      properties:
 | 
			
		||||
        name:
 | 
			
		||||
          type: string
 | 
			
		||||
          format: ident
 | 
			
		||||
        smtpHost:
 | 
			
		||||
          type: string
 | 
			
		||||
        smtpPort:
 | 
			
		||||
          type: integer
 | 
			
		||||
          format: int32
 | 
			
		||||
        smtpUser:
 | 
			
		||||
          type: string
 | 
			
		||||
        smtpPassword:
 | 
			
		||||
          type: string
 | 
			
		||||
          format: password
 | 
			
		||||
        from:
 | 
			
		||||
          type: string
 | 
			
		||||
        replyTo:
 | 
			
		||||
          type: string
 | 
			
		||||
        sslType:
 | 
			
		||||
          type: string
 | 
			
		||||
        ignoreCertificates:
 | 
			
		||||
          type: boolean
 | 
			
		||||
    CheckFileResult:
 | 
			
		||||
      description: |
 | 
			
		||||
        Results when searching for file checksums.
 | 
			
		||||
@@ -2157,7 +2448,7 @@ components:
 | 
			
		||||
    id:
 | 
			
		||||
      name: id
 | 
			
		||||
      in: path
 | 
			
		||||
      description: A identifier
 | 
			
		||||
      description: An identifier
 | 
			
		||||
      required: true
 | 
			
		||||
      schema:
 | 
			
		||||
        type: string
 | 
			
		||||
@@ -2182,3 +2473,17 @@ components:
 | 
			
		||||
      required: false
 | 
			
		||||
      schema:
 | 
			
		||||
        type: string
 | 
			
		||||
    name:
 | 
			
		||||
      name: name
 | 
			
		||||
      in: path
 | 
			
		||||
      description: An e-mail connection name
 | 
			
		||||
      required: true
 | 
			
		||||
      schema:
 | 
			
		||||
        type: string
 | 
			
		||||
    mailId:
 | 
			
		||||
      name: mailId
 | 
			
		||||
      in: path
 | 
			
		||||
      description: The id of a sent mail.
 | 
			
		||||
      required: true
 | 
			
		||||
      schema:
 | 
			
		||||
        type: string
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@
 | 
			
		||||
  </appender>
 | 
			
		||||
 | 
			
		||||
  <logger name="docspell" level="debug" />
 | 
			
		||||
  <logger name="emil" level="debug"/>
 | 
			
		||||
 | 
			
		||||
  <root level="INFO">
 | 
			
		||||
    <appender-ref ref="STDOUT" />
 | 
			
		||||
  </root>
 | 
			
		||||
 
 | 
			
		||||
@@ -57,19 +57,22 @@ object RestServer {
 | 
			
		||||
      token: AuthToken
 | 
			
		||||
  ): HttpRoutes[F] =
 | 
			
		||||
    Router(
 | 
			
		||||
      "auth"         -> LoginRoutes.session(restApp.backend.login, cfg),
 | 
			
		||||
      "tag"          -> TagRoutes(restApp.backend, token),
 | 
			
		||||
      "equipment"    -> EquipmentRoutes(restApp.backend, token),
 | 
			
		||||
      "organization" -> OrganizationRoutes(restApp.backend, token),
 | 
			
		||||
      "person"       -> PersonRoutes(restApp.backend, token),
 | 
			
		||||
      "source"       -> SourceRoutes(restApp.backend, token),
 | 
			
		||||
      "user"         -> UserRoutes(restApp.backend, token),
 | 
			
		||||
      "collective"   -> CollectiveRoutes(restApp.backend, token),
 | 
			
		||||
      "queue"        -> JobQueueRoutes(restApp.backend, token),
 | 
			
		||||
      "item"         -> ItemRoutes(restApp.backend, token),
 | 
			
		||||
      "attachment"   -> AttachmentRoutes(restApp.backend, token),
 | 
			
		||||
      "upload"       -> UploadRoutes.secured(restApp.backend, cfg, token),
 | 
			
		||||
      "checkfile"    -> CheckFileRoutes.secured(restApp.backend, token)
 | 
			
		||||
      "auth"           -> LoginRoutes.session(restApp.backend.login, cfg),
 | 
			
		||||
      "tag"            -> TagRoutes(restApp.backend, token),
 | 
			
		||||
      "equipment"      -> EquipmentRoutes(restApp.backend, token),
 | 
			
		||||
      "organization"   -> OrganizationRoutes(restApp.backend, token),
 | 
			
		||||
      "person"         -> PersonRoutes(restApp.backend, token),
 | 
			
		||||
      "source"         -> SourceRoutes(restApp.backend, token),
 | 
			
		||||
      "user"           -> UserRoutes(restApp.backend, token),
 | 
			
		||||
      "collective"     -> CollectiveRoutes(restApp.backend, token),
 | 
			
		||||
      "queue"          -> JobQueueRoutes(restApp.backend, token),
 | 
			
		||||
      "item"           -> ItemRoutes(restApp.backend, token),
 | 
			
		||||
      "attachment"     -> AttachmentRoutes(restApp.backend, token),
 | 
			
		||||
      "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/sent"     -> SentMailRoutes(restApp.backend, token)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
  def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
 | 
			
		||||
 
 | 
			
		||||
@@ -96,15 +96,15 @@ object ItemRoutes {
 | 
			
		||||
      case req @ POST -> Root / Ident(id) / "notes" =>
 | 
			
		||||
        for {
 | 
			
		||||
          text <- req.as[OptionalText]
 | 
			
		||||
          res  <- backend.item.setNotes(id, text.text, user.account.collective)
 | 
			
		||||
          resp <- Ok(Conversions.basicResult(res, "Concerned equipment updated"))
 | 
			
		||||
          res  <- backend.item.setNotes(id, text.text.notEmpty, user.account.collective)
 | 
			
		||||
          resp <- Ok(Conversions.basicResult(res, "Notes updated"))
 | 
			
		||||
        } yield resp
 | 
			
		||||
 | 
			
		||||
      case req @ POST -> Root / Ident(id) / "name" =>
 | 
			
		||||
        for {
 | 
			
		||||
          text <- req.as[OptionalText]
 | 
			
		||||
          res  <- backend.item.setName(id, text.text.getOrElse(""), user.account.collective)
 | 
			
		||||
          resp <- Ok(Conversions.basicResult(res, "Concerned equipment updated"))
 | 
			
		||||
          res  <- backend.item.setName(id, text.text.notEmpty.getOrElse(""), user.account.collective)
 | 
			
		||||
          resp <- Ok(Conversions.basicResult(res, "Name updated"))
 | 
			
		||||
        } yield resp
 | 
			
		||||
 | 
			
		||||
      case req @ POST -> Root / Ident(id) / "duedate" =>
 | 
			
		||||
@@ -138,4 +138,10 @@ object ItemRoutes {
 | 
			
		||||
        } yield resp
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  final implicit class OptionString(opt: Option[String]) {
 | 
			
		||||
    def notEmpty: Option[String] =
 | 
			
		||||
      opt.map(_.trim).filter(_.nonEmpty)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,56 @@
 | 
			
		||||
package docspell.restserver.routes
 | 
			
		||||
 | 
			
		||||
import cats.effect._
 | 
			
		||||
import cats.implicits._
 | 
			
		||||
import org.http4s._
 | 
			
		||||
import org.http4s.dsl.Http4sDsl
 | 
			
		||||
import org.http4s.circe.CirceEntityEncoder._
 | 
			
		||||
import org.http4s.circe.CirceEntityDecoder._
 | 
			
		||||
 | 
			
		||||
import docspell.backend.BackendApp
 | 
			
		||||
import docspell.backend.auth.AuthToken
 | 
			
		||||
import docspell.backend.ops.OMail.{AttachSelection, ItemMail}
 | 
			
		||||
import docspell.backend.ops.SendResult
 | 
			
		||||
import docspell.common._
 | 
			
		||||
import docspell.restapi.model._
 | 
			
		||||
import docspell.store.EmilUtil
 | 
			
		||||
 | 
			
		||||
object MailSendRoutes {
 | 
			
		||||
 | 
			
		||||
  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
 | 
			
		||||
    val dsl = new Http4sDsl[F] {}
 | 
			
		||||
    import dsl._
 | 
			
		||||
 | 
			
		||||
    HttpRoutes.of {
 | 
			
		||||
      case req @ POST -> Root / Ident(name) / Ident(id) =>
 | 
			
		||||
        for {
 | 
			
		||||
          in <- req.as[SimpleMail]
 | 
			
		||||
          mail = convertIn(id, in)
 | 
			
		||||
          res <- mail.traverse(m => backend.mail.sendMail(user.account, name, m))
 | 
			
		||||
          resp <- res.fold(
 | 
			
		||||
            err => Ok(BasicResult(false, s"Invalid mail data: $err")),
 | 
			
		||||
            res => Ok(convertOut(res))
 | 
			
		||||
          )
 | 
			
		||||
        } yield resp
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def convertIn(item: Ident, s: SimpleMail): Either[String, ItemMail] =
 | 
			
		||||
    for {
 | 
			
		||||
      rec     <- s.recipients.traverse(EmilUtil.readMailAddress)
 | 
			
		||||
      fileIds <- s.attachmentIds.traverse(Ident.fromString)
 | 
			
		||||
      sel = if (s.addAllAttachments) AttachSelection.All else AttachSelection.Selected(fileIds)
 | 
			
		||||
    } yield ItemMail(item, s.subject, rec, s.body, sel)
 | 
			
		||||
 | 
			
		||||
  def convertOut(res: SendResult): BasicResult =
 | 
			
		||||
    res match {
 | 
			
		||||
      case SendResult.Success(_) =>
 | 
			
		||||
        BasicResult(true, "Mail sent.")
 | 
			
		||||
      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.")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,123 @@
 | 
			
		||||
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 org.http4s.circe.CirceEntityDecoder._
 | 
			
		||||
import emil.MailAddress
 | 
			
		||||
 | 
			
		||||
import docspell.backend.BackendApp
 | 
			
		||||
import docspell.backend.auth.AuthToken
 | 
			
		||||
import docspell.backend.ops.OMail
 | 
			
		||||
import docspell.common._
 | 
			
		||||
import docspell.restapi.model._
 | 
			
		||||
import docspell.store.records.RUserEmail
 | 
			
		||||
import docspell.store.EmilUtil
 | 
			
		||||
import docspell.restserver.conv.Conversions
 | 
			
		||||
 | 
			
		||||
object MailSettingsRoutes {
 | 
			
		||||
 | 
			
		||||
  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
 | 
			
		||||
    val dsl = new Http4sDsl[F] {}
 | 
			
		||||
    import dsl._
 | 
			
		||||
 | 
			
		||||
    HttpRoutes.of {
 | 
			
		||||
      case req @ GET -> Root =>
 | 
			
		||||
        val q = req.params.get("q").map(_.trim).filter(_.nonEmpty)
 | 
			
		||||
        for {
 | 
			
		||||
          list <- backend.mail.getSettings(user.account, q)
 | 
			
		||||
          res = list.map(convert)
 | 
			
		||||
          resp <- Ok(EmailSettingsList(res.toList))
 | 
			
		||||
        } yield resp
 | 
			
		||||
 | 
			
		||||
      case GET -> Root / Ident(name) =>
 | 
			
		||||
        (for {
 | 
			
		||||
          ems  <- backend.mail.findSettings(user.account, name)
 | 
			
		||||
          resp <- OptionT.liftF(Ok(convert(ems)))
 | 
			
		||||
        } yield resp).getOrElseF(NotFound())
 | 
			
		||||
 | 
			
		||||
      case req @ POST -> Root =>
 | 
			
		||||
        (for {
 | 
			
		||||
          in <- OptionT.liftF(req.as[EmailSettings])
 | 
			
		||||
          ru = makeSettings(in)
 | 
			
		||||
          up <- OptionT.liftF(ru.traverse(r => backend.mail.createSettings(user.account, r)))
 | 
			
		||||
          resp <- OptionT.liftF(
 | 
			
		||||
            Ok(
 | 
			
		||||
              up.fold(
 | 
			
		||||
                err => BasicResult(false, err),
 | 
			
		||||
                ar => Conversions.basicResult(ar, "Mail settings stored.")
 | 
			
		||||
              )
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
        } yield resp).getOrElseF(NotFound())
 | 
			
		||||
 | 
			
		||||
      case req @ PUT -> Root / Ident(name) =>
 | 
			
		||||
        (for {
 | 
			
		||||
          in <- OptionT.liftF(req.as[EmailSettings])
 | 
			
		||||
          ru = makeSettings(in)
 | 
			
		||||
          up <- OptionT.liftF(ru.traverse(r => backend.mail.updateSettings(user.account, name, r)))
 | 
			
		||||
          resp <- OptionT.liftF(
 | 
			
		||||
            Ok(
 | 
			
		||||
              up.fold(
 | 
			
		||||
                err => BasicResult(false, err),
 | 
			
		||||
                n =>
 | 
			
		||||
                  if (n > 0) BasicResult(true, "Mail settings stored.")
 | 
			
		||||
                  else BasicResult(false, "Mail settings could not be saved")
 | 
			
		||||
              )
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
        } yield resp).getOrElseF(NotFound())
 | 
			
		||||
 | 
			
		||||
      case DELETE -> Root / Ident(name) =>
 | 
			
		||||
        for {
 | 
			
		||||
          n <- backend.mail.deleteSettings(user.account, name)
 | 
			
		||||
          resp <- Ok(
 | 
			
		||||
            if (n > 0) BasicResult(true, "Mail settings removed")
 | 
			
		||||
            else BasicResult(false, "Mail settings could not be removed")
 | 
			
		||||
          )
 | 
			
		||||
        } yield resp
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def convert(ru: RUserEmail): EmailSettings =
 | 
			
		||||
    EmailSettings(
 | 
			
		||||
      ru.name,
 | 
			
		||||
      ru.smtpHost,
 | 
			
		||||
      ru.smtpPort,
 | 
			
		||||
      ru.smtpUser,
 | 
			
		||||
      ru.smtpPassword,
 | 
			
		||||
      EmilUtil.mailAddressString(ru.mailFrom),
 | 
			
		||||
      ru.mailReplyTo.map(EmilUtil.mailAddressString _),
 | 
			
		||||
      EmilUtil.sslTypeString(ru.smtpSsl),
 | 
			
		||||
      !ru.smtpCertCheck
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
  def makeSettings(ems: EmailSettings): Either[String, OMail.SmtpSettings] = {
 | 
			
		||||
    def readMail(str: String): Either[String, MailAddress] =
 | 
			
		||||
      EmilUtil.readMailAddress(str).left.map(err => s"E-Mail address '$str' invalid: $err")
 | 
			
		||||
 | 
			
		||||
    def readMailOpt(str: Option[String]): Either[String, Option[MailAddress]] =
 | 
			
		||||
      str.traverse(readMail)
 | 
			
		||||
 | 
			
		||||
    for {
 | 
			
		||||
      from <- readMail(ems.from)
 | 
			
		||||
      repl <- readMailOpt(ems.replyTo)
 | 
			
		||||
      sslt <- EmilUtil.readSSLType(ems.sslType)
 | 
			
		||||
    } yield OMail.SmtpSettings(
 | 
			
		||||
      ems.name,
 | 
			
		||||
      ems.smtpHost,
 | 
			
		||||
      ems.smtpPort,
 | 
			
		||||
      ems.smtpUser,
 | 
			
		||||
      ems.smtpPassword,
 | 
			
		||||
      sslt,
 | 
			
		||||
      !ems.ignoreCertificates,
 | 
			
		||||
      from,
 | 
			
		||||
      repl
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
CREATE TABLE "useremail" (
 | 
			
		||||
  "id" varchar(254) not null primary key,
 | 
			
		||||
  "uid" varchar(254) not null,
 | 
			
		||||
  "name" varchar(254) not null,
 | 
			
		||||
  "smtp_host" varchar(254) not null,
 | 
			
		||||
  "smtp_port" int,
 | 
			
		||||
  "smtp_user" varchar(254),
 | 
			
		||||
  "smtp_password" varchar(254),
 | 
			
		||||
  "smtp_ssl" varchar(254) not null,
 | 
			
		||||
  "smtp_certcheck" boolean not null,
 | 
			
		||||
  "mail_from" varchar(254) not null,
 | 
			
		||||
  "mail_replyto" varchar(254),
 | 
			
		||||
  "created" timestamp not null,
 | 
			
		||||
  unique ("uid", "name"),
 | 
			
		||||
  foreign key ("uid") references "user_"("uid")
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE "sentmail" (
 | 
			
		||||
  "id" varchar(254) not null primary key,
 | 
			
		||||
  "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,
 | 
			
		||||
  "created" timestamp not null,
 | 
			
		||||
  foreign key("uid") references "user_"("uid")
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE "sentmailitem" (
 | 
			
		||||
  "id" varchar(254) not null primary key,
 | 
			
		||||
  "item_id" varchar(254) not null,
 | 
			
		||||
  "sentmail_id" varchar(254) not null,
 | 
			
		||||
  "created" timestamp not null,
 | 
			
		||||
  unique ("item_id", "sentmail_id"),
 | 
			
		||||
  foreign key("item_id") references "item"("itemid"),
 | 
			
		||||
  foreign key("sentmail_id") references "sentmail"("id")
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										38
									
								
								modules/store/src/main/scala/docspell/store/EmilUtil.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								modules/store/src/main/scala/docspell/store/EmilUtil.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
package docspell.store
 | 
			
		||||
 | 
			
		||||
import cats.implicits._
 | 
			
		||||
import emil._
 | 
			
		||||
import emil.javamail.syntax._
 | 
			
		||||
 | 
			
		||||
object EmilUtil {
 | 
			
		||||
 | 
			
		||||
  def readSSLType(str: String): Either[String, SSLType] =
 | 
			
		||||
    str.toLowerCase match {
 | 
			
		||||
      case "ssl"      => Right(SSLType.SSL)
 | 
			
		||||
      case "starttls" => Right(SSLType.StartTLS)
 | 
			
		||||
      case "none"     => Right(SSLType.NoEncryption)
 | 
			
		||||
      case _          => Left(s"Invalid ssl-type: $str")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  def unsafeReadSSLType(str: String): SSLType =
 | 
			
		||||
    readSSLType(str).fold(sys.error, identity)
 | 
			
		||||
 | 
			
		||||
  def sslTypeString(st: SSLType): String =
 | 
			
		||||
    st match {
 | 
			
		||||
      case SSLType.SSL          => "ssl"
 | 
			
		||||
      case SSLType.StartTLS     => "starttls"
 | 
			
		||||
      case SSLType.NoEncryption => "none"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  def readMailAddress(str: String): Either[String, MailAddress] =
 | 
			
		||||
    MailAddress.parse(str)
 | 
			
		||||
 | 
			
		||||
  def unsafeReadMailAddress(str: String): MailAddress =
 | 
			
		||||
    readMailAddress(str).fold(sys.error, identity)
 | 
			
		||||
 | 
			
		||||
  def readMultipleAddresses(str: String): Either[String, List[MailAddress]] =
 | 
			
		||||
    str.split(',').toList.map(_.trim).traverse(readMailAddress)
 | 
			
		||||
 | 
			
		||||
  def mailAddressString(ma: MailAddress): String =
 | 
			
		||||
    ma.asUnicodeString
 | 
			
		||||
}
 | 
			
		||||
@@ -2,16 +2,15 @@ package docspell.store.impl
 | 
			
		||||
 | 
			
		||||
import java.time.format.DateTimeFormatter
 | 
			
		||||
import java.time.{Instant, LocalDate}
 | 
			
		||||
 | 
			
		||||
import docspell.common.Timestamp
 | 
			
		||||
 | 
			
		||||
import docspell.common._
 | 
			
		||||
import io.circe.{Decoder, Encoder}
 | 
			
		||||
import doobie._
 | 
			
		||||
//import doobie.implicits.javatime._
 | 
			
		||||
import doobie.implicits.legacy.instant._
 | 
			
		||||
import doobie.util.log.Success
 | 
			
		||||
import io.circe.{Decoder, Encoder}
 | 
			
		||||
import emil.{MailAddress, SSLType}
 | 
			
		||||
 | 
			
		||||
import docspell.common._
 | 
			
		||||
import docspell.common.syntax.all._
 | 
			
		||||
import docspell.store.EmilUtil
 | 
			
		||||
 | 
			
		||||
trait DoobieMeta {
 | 
			
		||||
 | 
			
		||||
@@ -88,9 +87,21 @@ trait DoobieMeta {
 | 
			
		||||
 | 
			
		||||
  implicit val metaLanguage: Meta[Language] =
 | 
			
		||||
    Meta[String].imap(Language.unsafe)(_.iso3)
 | 
			
		||||
 | 
			
		||||
  implicit val sslType: Meta[SSLType] =
 | 
			
		||||
    Meta[String].imap(EmilUtil.unsafeReadSSLType)(EmilUtil.sslTypeString)
 | 
			
		||||
 | 
			
		||||
  implicit val mailAddress: Meta[MailAddress] =
 | 
			
		||||
    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 {
 | 
			
		||||
  import org.log4s._
 | 
			
		||||
  private val logger = getLogger
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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._
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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] =
 | 
			
		||||
    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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,100 @@
 | 
			
		||||
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,
 | 
			
		||||
    uid: Ident,
 | 
			
		||||
    messageId: String,
 | 
			
		||||
    sender: MailAddress,
 | 
			
		||||
    connName: Ident,
 | 
			
		||||
    subject: String,
 | 
			
		||||
    recipients: List[MailAddress],
 | 
			
		||||
    body: String,
 | 
			
		||||
    created: Timestamp
 | 
			
		||||
) {}
 | 
			
		||||
 | 
			
		||||
object RSentMail {
 | 
			
		||||
 | 
			
		||||
  def apply[F[_]: Sync](
 | 
			
		||||
      uid: Ident,
 | 
			
		||||
      messageId: String,
 | 
			
		||||
      sender: MailAddress,
 | 
			
		||||
      connName: Ident,
 | 
			
		||||
      subject: String,
 | 
			
		||||
      recipients: List[MailAddress],
 | 
			
		||||
      body: String
 | 
			
		||||
  ): F[RSentMail] =
 | 
			
		||||
    for {
 | 
			
		||||
      id  <- Ident.randomId[F]
 | 
			
		||||
      now <- Timestamp.current[F]
 | 
			
		||||
    } 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, connName, 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 connName   = Column("conn_name")
 | 
			
		||||
    val subject    = Column("subject")
 | 
			
		||||
    val recipients = Column("recipients")
 | 
			
		||||
    val body       = Column("body")
 | 
			
		||||
    val created    = Column("created")
 | 
			
		||||
 | 
			
		||||
    val all = List(
 | 
			
		||||
      id,
 | 
			
		||||
      uid,
 | 
			
		||||
      messageId,
 | 
			
		||||
      sender,
 | 
			
		||||
      connName,
 | 
			
		||||
      subject,
 | 
			
		||||
      recipients,
 | 
			
		||||
      body,
 | 
			
		||||
      created
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  import Columns._
 | 
			
		||||
 | 
			
		||||
  def insert(v: RSentMail): ConnectionIO[Int] =
 | 
			
		||||
    insertRow(
 | 
			
		||||
      table,
 | 
			
		||||
      all,
 | 
			
		||||
      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
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,57 @@
 | 
			
		||||
package docspell.store.records
 | 
			
		||||
 | 
			
		||||
import cats.effect._
 | 
			
		||||
import cats.implicits._
 | 
			
		||||
import doobie._
 | 
			
		||||
import doobie.implicits._
 | 
			
		||||
import docspell.common._
 | 
			
		||||
import docspell.store.impl.Column
 | 
			
		||||
import docspell.store.impl.Implicits._
 | 
			
		||||
 | 
			
		||||
case class RSentMailItem(
 | 
			
		||||
    id: Ident,
 | 
			
		||||
    itemId: Ident,
 | 
			
		||||
    sentMailId: Ident,
 | 
			
		||||
    created: Timestamp
 | 
			
		||||
) {}
 | 
			
		||||
 | 
			
		||||
object RSentMailItem {
 | 
			
		||||
 | 
			
		||||
  def apply[F[_]: Sync](
 | 
			
		||||
      itemId: Ident,
 | 
			
		||||
      sentmailId: Ident,
 | 
			
		||||
      created: Option[Timestamp] = None
 | 
			
		||||
  ): F[RSentMailItem] =
 | 
			
		||||
    for {
 | 
			
		||||
      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 {
 | 
			
		||||
    val id         = Column("id")
 | 
			
		||||
    val itemId     = Column("item_id")
 | 
			
		||||
    val sentMailId = Column("sentmail_id")
 | 
			
		||||
    val created    = Column("created")
 | 
			
		||||
 | 
			
		||||
    val all = List(
 | 
			
		||||
      id,
 | 
			
		||||
      itemId,
 | 
			
		||||
      sentMailId,
 | 
			
		||||
      created
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  import Columns._
 | 
			
		||||
 | 
			
		||||
  def insert(v: RSentMailItem): ConnectionIO[Int] =
 | 
			
		||||
    insertRow(
 | 
			
		||||
      table,
 | 
			
		||||
      all,
 | 
			
		||||
      sql"${v.id},${v.itemId},${v.sentMailId},${v.created}"
 | 
			
		||||
    ).update.run
 | 
			
		||||
 | 
			
		||||
  def deleteMail(mailId: Ident): ConnectionIO[Int] =
 | 
			
		||||
    deleteFrom(table, sentMailId.is(mailId)).update.run
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,212 @@
 | 
			
		||||
package docspell.store.records
 | 
			
		||||
 | 
			
		||||
import doobie._
 | 
			
		||||
import doobie.implicits._
 | 
			
		||||
import cats.effect._
 | 
			
		||||
import cats.implicits._
 | 
			
		||||
import cats.data.OptionT
 | 
			
		||||
import docspell.common._
 | 
			
		||||
import docspell.store.impl.Column
 | 
			
		||||
import docspell.store.impl.Implicits._
 | 
			
		||||
import emil.{MailAddress, MailConfig, SSLType}
 | 
			
		||||
 | 
			
		||||
case class RUserEmail(
 | 
			
		||||
    id: Ident,
 | 
			
		||||
    uid: Ident,
 | 
			
		||||
    name: Ident,
 | 
			
		||||
    smtpHost: String,
 | 
			
		||||
    smtpPort: Option[Int],
 | 
			
		||||
    smtpUser: Option[String],
 | 
			
		||||
    smtpPassword: Option[Password],
 | 
			
		||||
    smtpSsl: SSLType,
 | 
			
		||||
    smtpCertCheck: Boolean,
 | 
			
		||||
    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 {
 | 
			
		||||
 | 
			
		||||
  def apply[F[_]: Sync](
 | 
			
		||||
      uid: Ident,
 | 
			
		||||
      name: Ident,
 | 
			
		||||
      smtpHost: String,
 | 
			
		||||
      smtpPort: Option[Int],
 | 
			
		||||
      smtpUser: Option[String],
 | 
			
		||||
      smtpPassword: Option[Password],
 | 
			
		||||
      smtpSsl: SSLType,
 | 
			
		||||
      smtpCertCheck: Boolean,
 | 
			
		||||
      mailFrom: MailAddress,
 | 
			
		||||
      mailReplyTo: Option[MailAddress]
 | 
			
		||||
  ): F[RUserEmail] =
 | 
			
		||||
    for {
 | 
			
		||||
      now <- Timestamp.current[F]
 | 
			
		||||
      id  <- Ident.randomId[F]
 | 
			
		||||
    } yield RUserEmail(
 | 
			
		||||
      id,
 | 
			
		||||
      uid,
 | 
			
		||||
      name,
 | 
			
		||||
      smtpHost,
 | 
			
		||||
      smtpPort,
 | 
			
		||||
      smtpUser,
 | 
			
		||||
      smtpPassword,
 | 
			
		||||
      smtpSsl,
 | 
			
		||||
      smtpCertCheck,
 | 
			
		||||
      mailFrom,
 | 
			
		||||
      mailReplyTo,
 | 
			
		||||
      now
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
  def fromAccount(
 | 
			
		||||
      accId: AccountId,
 | 
			
		||||
      name: Ident,
 | 
			
		||||
      smtpHost: String,
 | 
			
		||||
      smtpPort: Option[Int],
 | 
			
		||||
      smtpUser: Option[String],
 | 
			
		||||
      smtpPassword: Option[Password],
 | 
			
		||||
      smtpSsl: SSLType,
 | 
			
		||||
      smtpCertCheck: Boolean,
 | 
			
		||||
      mailFrom: MailAddress,
 | 
			
		||||
      mailReplyTo: Option[MailAddress]
 | 
			
		||||
  ): OptionT[ConnectionIO, RUserEmail] =
 | 
			
		||||
    for {
 | 
			
		||||
      now  <- OptionT.liftF(Timestamp.current[ConnectionIO])
 | 
			
		||||
      id   <- OptionT.liftF(Ident.randomId[ConnectionIO])
 | 
			
		||||
      user <- OptionT(RUser.findByAccount(accId))
 | 
			
		||||
    } yield RUserEmail(
 | 
			
		||||
      id,
 | 
			
		||||
      user.uid,
 | 
			
		||||
      name,
 | 
			
		||||
      smtpHost,
 | 
			
		||||
      smtpPort,
 | 
			
		||||
      smtpUser,
 | 
			
		||||
      smtpPassword,
 | 
			
		||||
      smtpSsl,
 | 
			
		||||
      smtpCertCheck,
 | 
			
		||||
      mailFrom,
 | 
			
		||||
      mailReplyTo,
 | 
			
		||||
      now
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
  val table = fr"useremail"
 | 
			
		||||
 | 
			
		||||
  object Columns {
 | 
			
		||||
    val id            = Column("id")
 | 
			
		||||
    val uid           = Column("uid")
 | 
			
		||||
    val name          = Column("name")
 | 
			
		||||
    val smtpHost      = Column("smtp_host")
 | 
			
		||||
    val smtpPort      = Column("smtp_port")
 | 
			
		||||
    val smtpUser      = Column("smtp_user")
 | 
			
		||||
    val smtpPass      = Column("smtp_password")
 | 
			
		||||
    val smtpSsl       = Column("smtp_ssl")
 | 
			
		||||
    val smtpCertCheck = Column("smtp_certcheck")
 | 
			
		||||
    val mailFrom      = Column("mail_from")
 | 
			
		||||
    val mailReplyTo   = Column("mail_replyto")
 | 
			
		||||
    val created       = Column("created")
 | 
			
		||||
 | 
			
		||||
    val all = List(
 | 
			
		||||
      id,
 | 
			
		||||
      uid,
 | 
			
		||||
      name,
 | 
			
		||||
      smtpHost,
 | 
			
		||||
      smtpPort,
 | 
			
		||||
      smtpUser,
 | 
			
		||||
      smtpPass,
 | 
			
		||||
      smtpSsl,
 | 
			
		||||
      smtpCertCheck,
 | 
			
		||||
      mailFrom,
 | 
			
		||||
      mailReplyTo,
 | 
			
		||||
      created
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  import Columns._
 | 
			
		||||
 | 
			
		||||
  def insert(v: RUserEmail): ConnectionIO[Int] =
 | 
			
		||||
    insertRow(
 | 
			
		||||
      table,
 | 
			
		||||
      all,
 | 
			
		||||
      sql"${v.id},${v.uid},${v.name},${v.smtpHost},${v.smtpPort},${v.smtpUser},${v.smtpPassword},${v.smtpSsl},${v.smtpCertCheck},${v.mailFrom},${v.mailReplyTo},${v.created}"
 | 
			
		||||
    ).update.run
 | 
			
		||||
 | 
			
		||||
  def update(eId: Ident, v: RUserEmail): ConnectionIO[Int] =
 | 
			
		||||
    updateRow(
 | 
			
		||||
      table,
 | 
			
		||||
      id.is(eId),
 | 
			
		||||
      commas(
 | 
			
		||||
        name.setTo(v.name),
 | 
			
		||||
        smtpHost.setTo(v.smtpHost),
 | 
			
		||||
        smtpPort.setTo(v.smtpPort),
 | 
			
		||||
        smtpUser.setTo(v.smtpUser),
 | 
			
		||||
        smtpPass.setTo(v.smtpPassword),
 | 
			
		||||
        smtpSsl.setTo(v.smtpSsl),
 | 
			
		||||
        smtpCertCheck.setTo(v.smtpCertCheck),
 | 
			
		||||
        mailFrom.setTo(v.mailFrom),
 | 
			
		||||
        mailReplyTo.setTo(v.mailReplyTo)
 | 
			
		||||
      )
 | 
			
		||||
    ).update.run
 | 
			
		||||
 | 
			
		||||
  def findByUser(userId: Ident): ConnectionIO[Vector[RUserEmail]] =
 | 
			
		||||
    selectSimple(all, table, uid.is(userId)).query[RUserEmail].to[Vector]
 | 
			
		||||
 | 
			
		||||
  private def findByAccount0(
 | 
			
		||||
      accId: AccountId,
 | 
			
		||||
      nameQ: Option[String],
 | 
			
		||||
      exact: Boolean
 | 
			
		||||
  ): Query0[RUserEmail] = {
 | 
			
		||||
    val mUid   = uid.prefix("m")
 | 
			
		||||
    val mName  = name.prefix("m")
 | 
			
		||||
    val uId    = RUser.Columns.uid.prefix("u")
 | 
			
		||||
    val uColl  = RUser.Columns.cid.prefix("u")
 | 
			
		||||
    val uLogin = RUser.Columns.login.prefix("u")
 | 
			
		||||
    val from   = table ++ fr"m INNER JOIN" ++ RUser.table ++ fr"u ON" ++ mUid.is(uId)
 | 
			
		||||
    val cond = Seq(uColl.is(accId.collective), uLogin.is(accId.user)) ++ (nameQ match {
 | 
			
		||||
      case Some(str) if exact  => Seq(mName.is(str))
 | 
			
		||||
      case Some(str) if !exact => Seq(mName.lowerLike(s"%${str.toLowerCase}%"))
 | 
			
		||||
      case None                => Seq.empty
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    (selectSimple(all.map(_.prefix("m")), from, and(cond)) ++ orderBy(mName.f)).query[RUserEmail]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def findByAccount(
 | 
			
		||||
      accId: AccountId,
 | 
			
		||||
      nameQ: Option[String]
 | 
			
		||||
  ): ConnectionIO[Vector[RUserEmail]] =
 | 
			
		||||
    findByAccount0(accId, nameQ, false).to[Vector]
 | 
			
		||||
 | 
			
		||||
  def getByName(accId: AccountId, name: Ident): ConnectionIO[Option[RUserEmail]] =
 | 
			
		||||
    findByAccount0(accId, Some(name.id), true).option
 | 
			
		||||
 | 
			
		||||
  def delete(accId: AccountId, connName: Ident): ConnectionIO[Int] = {
 | 
			
		||||
    val uId    = RUser.Columns.uid
 | 
			
		||||
    val uColl  = RUser.Columns.cid
 | 
			
		||||
    val uLogin = RUser.Columns.login
 | 
			
		||||
    val cond   = Seq(uColl.is(accId.collective), uLogin.is(accId.user))
 | 
			
		||||
 | 
			
		||||
    deleteFrom(
 | 
			
		||||
      table,
 | 
			
		||||
      fr"uid in (" ++ selectSimple(Seq(uId), RUser.table, and(cond)) ++ fr") AND" ++ name.is(
 | 
			
		||||
        connName
 | 
			
		||||
      )
 | 
			
		||||
    ).update.run
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def exists(accId: AccountId, name: Ident): ConnectionIO[Boolean] =
 | 
			
		||||
    getByName(accId, name).map(_.isDefined)
 | 
			
		||||
 | 
			
		||||
  def exists(userId: Ident, connName: Ident): ConnectionIO[Boolean] =
 | 
			
		||||
    selectCount(id, table, and(uid.is(userId), name.is(connName))).query[Int].unique.map(_ > 0)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
module Api exposing
 | 
			
		||||
    ( cancelJob
 | 
			
		||||
    , changePassword
 | 
			
		||||
    , createMailSettings
 | 
			
		||||
    , deleteEquip
 | 
			
		||||
    , deleteItem
 | 
			
		||||
    , deleteMailSettings
 | 
			
		||||
    , deleteOrg
 | 
			
		||||
    , deletePerson
 | 
			
		||||
    , deleteSource
 | 
			
		||||
@@ -15,10 +17,12 @@ module Api exposing
 | 
			
		||||
    , getItemProposals
 | 
			
		||||
    , getJobQueueState
 | 
			
		||||
    , getJobQueueStateIn
 | 
			
		||||
    , getMailSettings
 | 
			
		||||
    , getOrgLight
 | 
			
		||||
    , getOrganizations
 | 
			
		||||
    , getPersons
 | 
			
		||||
    , getPersonsLight
 | 
			
		||||
    , getSentMails
 | 
			
		||||
    , getSources
 | 
			
		||||
    , getTags
 | 
			
		||||
    , getUsers
 | 
			
		||||
@@ -37,6 +41,7 @@ module Api exposing
 | 
			
		||||
    , putUser
 | 
			
		||||
    , refreshSession
 | 
			
		||||
    , register
 | 
			
		||||
    , sendMail
 | 
			
		||||
    , setCollectiveSettings
 | 
			
		||||
    , setConcEquip
 | 
			
		||||
    , setConcPerson
 | 
			
		||||
@@ -60,6 +65,8 @@ import Api.Model.BasicResult exposing (BasicResult)
 | 
			
		||||
import Api.Model.Collective exposing (Collective)
 | 
			
		||||
import Api.Model.CollectiveSettings exposing (CollectiveSettings)
 | 
			
		||||
import Api.Model.DirectionValue exposing (DirectionValue)
 | 
			
		||||
import Api.Model.EmailSettings exposing (EmailSettings)
 | 
			
		||||
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
 | 
			
		||||
import Api.Model.Equipment exposing (Equipment)
 | 
			
		||||
import Api.Model.EquipmentList exposing (EquipmentList)
 | 
			
		||||
import Api.Model.GenInvite exposing (GenInvite)
 | 
			
		||||
@@ -81,6 +88,8 @@ import Api.Model.Person exposing (Person)
 | 
			
		||||
import Api.Model.PersonList exposing (PersonList)
 | 
			
		||||
import Api.Model.ReferenceList exposing (ReferenceList)
 | 
			
		||||
import Api.Model.Registration exposing (Registration)
 | 
			
		||||
import Api.Model.SentMails exposing (SentMails)
 | 
			
		||||
import Api.Model.SimpleMail exposing (SimpleMail)
 | 
			
		||||
import Api.Model.Source exposing (Source)
 | 
			
		||||
import Api.Model.SourceList exposing (SourceList)
 | 
			
		||||
import Api.Model.Tag exposing (Tag)
 | 
			
		||||
@@ -99,6 +108,92 @@ import Util.File
 | 
			
		||||
import Util.Http as Http2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
--- Get Sent Mails
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
getSentMails :
 | 
			
		||||
    Flags
 | 
			
		||||
    -> String
 | 
			
		||||
    -> (Result Http.Error SentMails -> msg)
 | 
			
		||||
    -> Cmd msg
 | 
			
		||||
getSentMails flags item receive =
 | 
			
		||||
    Http2.authGet
 | 
			
		||||
        { url = flags.config.baseUrl ++ "/api/v1/sec/email/sent/item/" ++ item
 | 
			
		||||
        , account = getAccount flags
 | 
			
		||||
        , expect = Http.expectJson receive Api.Model.SentMails.decoder
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
--- Mail Send
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
sendMail :
 | 
			
		||||
    Flags
 | 
			
		||||
    -> { conn : String, item : String, mail : SimpleMail }
 | 
			
		||||
    -> (Result Http.Error BasicResult -> msg)
 | 
			
		||||
    -> Cmd msg
 | 
			
		||||
sendMail flags opts receive =
 | 
			
		||||
    Http2.authPost
 | 
			
		||||
        { url = flags.config.baseUrl ++ "/api/v1/sec/email/send/" ++ opts.conn ++ "/" ++ opts.item
 | 
			
		||||
        , account = getAccount flags
 | 
			
		||||
        , body = Http.jsonBody (Api.Model.SimpleMail.encode opts.mail)
 | 
			
		||||
        , expect = Http.expectJson receive Api.Model.BasicResult.decoder
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
--- Mail Settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
deleteMailSettings : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
 | 
			
		||||
deleteMailSettings flags name receive =
 | 
			
		||||
    Http2.authDelete
 | 
			
		||||
        { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/" ++ name
 | 
			
		||||
        , account = getAccount flags
 | 
			
		||||
        , expect = Http.expectJson receive Api.Model.BasicResult.decoder
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
getMailSettings : Flags -> String -> (Result Http.Error EmailSettingsList -> msg) -> Cmd msg
 | 
			
		||||
getMailSettings flags query receive =
 | 
			
		||||
    Http2.authGet
 | 
			
		||||
        { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings?q=" ++ Url.percentEncode query
 | 
			
		||||
        , account = getAccount flags
 | 
			
		||||
        , expect = Http.expectJson receive Api.Model.EmailSettingsList.decoder
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
createMailSettings :
 | 
			
		||||
    Flags
 | 
			
		||||
    -> Maybe String
 | 
			
		||||
    -> EmailSettings
 | 
			
		||||
    -> (Result Http.Error BasicResult -> msg)
 | 
			
		||||
    -> Cmd msg
 | 
			
		||||
createMailSettings flags mname ems receive =
 | 
			
		||||
    case mname of
 | 
			
		||||
        Just en ->
 | 
			
		||||
            Http2.authPut
 | 
			
		||||
                { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/" ++ en
 | 
			
		||||
                , account = getAccount flags
 | 
			
		||||
                , body = Http.jsonBody (Api.Model.EmailSettings.encode ems)
 | 
			
		||||
                , expect = Http.expectJson receive Api.Model.BasicResult.decoder
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
        Nothing ->
 | 
			
		||||
            Http2.authPost
 | 
			
		||||
                { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings"
 | 
			
		||||
                , account = getAccount flags
 | 
			
		||||
                , body = Http.jsonBody (Api.Model.EmailSettings.encode ems)
 | 
			
		||||
                , expect = Http.expectJson receive Api.Model.BasicResult.decoder
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
--- Upload
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
upload : Flags -> Maybe String -> ItemUploadMeta -> List File -> (String -> Result Http.Error BasicResult -> msg) -> List (Cmd msg)
 | 
			
		||||
upload flags sourceId meta files receive =
 | 
			
		||||
    let
 | 
			
		||||
 
 | 
			
		||||
@@ -395,7 +395,9 @@ viewSingle model =
 | 
			
		||||
                ]
 | 
			
		||||
 | 
			
		||||
        renderDefault =
 | 
			
		||||
            [ List.head model.selected |> Maybe.map renderClosed |> Maybe.withDefault (renderPlaceholder model)
 | 
			
		||||
            [ List.head model.selected
 | 
			
		||||
                |> Maybe.map renderClosed
 | 
			
		||||
                |> Maybe.withDefault (renderPlaceholder model)
 | 
			
		||||
            , renderMenu model
 | 
			
		||||
            ]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										264
									
								
								modules/webapp/src/main/elm/Comp/EmailSettingsForm.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								modules/webapp/src/main/elm/Comp/EmailSettingsForm.elm
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,264 @@
 | 
			
		||||
module Comp.EmailSettingsForm exposing
 | 
			
		||||
    ( Model
 | 
			
		||||
    , Msg
 | 
			
		||||
    , emptyModel
 | 
			
		||||
    , getSettings
 | 
			
		||||
    , init
 | 
			
		||||
    , isValid
 | 
			
		||||
    , update
 | 
			
		||||
    , view
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
import Api.Model.EmailSettings exposing (EmailSettings)
 | 
			
		||||
import Comp.Dropdown
 | 
			
		||||
import Comp.IntField
 | 
			
		||||
import Comp.PasswordInput
 | 
			
		||||
import Data.SSLType exposing (SSLType)
 | 
			
		||||
import Html exposing (..)
 | 
			
		||||
import Html.Attributes exposing (..)
 | 
			
		||||
import Html.Events exposing (onCheck, onInput)
 | 
			
		||||
import Util.Maybe
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type alias Model =
 | 
			
		||||
    { settings : EmailSettings
 | 
			
		||||
    , name : String
 | 
			
		||||
    , host : String
 | 
			
		||||
    , portField : Comp.IntField.Model
 | 
			
		||||
    , portNum : Maybe Int
 | 
			
		||||
    , user : Maybe String
 | 
			
		||||
    , passField : Comp.PasswordInput.Model
 | 
			
		||||
    , password : Maybe String
 | 
			
		||||
    , from : String
 | 
			
		||||
    , replyTo : Maybe String
 | 
			
		||||
    , sslType : Comp.Dropdown.Model SSLType
 | 
			
		||||
    , ignoreCertificates : Bool
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
emptyModel : Model
 | 
			
		||||
emptyModel =
 | 
			
		||||
    { settings = Api.Model.EmailSettings.empty
 | 
			
		||||
    , name = ""
 | 
			
		||||
    , host = ""
 | 
			
		||||
    , portField = Comp.IntField.init (Just 0) Nothing True "SMTP Port"
 | 
			
		||||
    , portNum = Nothing
 | 
			
		||||
    , user = Nothing
 | 
			
		||||
    , passField = Comp.PasswordInput.init
 | 
			
		||||
    , password = Nothing
 | 
			
		||||
    , from = ""
 | 
			
		||||
    , replyTo = Nothing
 | 
			
		||||
    , sslType =
 | 
			
		||||
        Comp.Dropdown.makeSingleList
 | 
			
		||||
            { makeOption = \s -> { value = Data.SSLType.toString s, text = Data.SSLType.label s }
 | 
			
		||||
            , placeholder = ""
 | 
			
		||||
            , options = Data.SSLType.all
 | 
			
		||||
            , selected = Just Data.SSLType.None
 | 
			
		||||
            }
 | 
			
		||||
    , ignoreCertificates = False
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
init : EmailSettings -> Model
 | 
			
		||||
init ems =
 | 
			
		||||
    { settings = ems
 | 
			
		||||
    , name = ems.name
 | 
			
		||||
    , host = ems.smtpHost
 | 
			
		||||
    , portField = Comp.IntField.init (Just 0) Nothing True "SMTP Port"
 | 
			
		||||
    , portNum = ems.smtpPort
 | 
			
		||||
    , user = ems.smtpUser
 | 
			
		||||
    , passField = Comp.PasswordInput.init
 | 
			
		||||
    , password = ems.smtpPassword
 | 
			
		||||
    , from = ems.from
 | 
			
		||||
    , replyTo = ems.replyTo
 | 
			
		||||
    , sslType =
 | 
			
		||||
        Comp.Dropdown.makeSingleList
 | 
			
		||||
            { makeOption = \s -> { value = Data.SSLType.toString s, text = Data.SSLType.label s }
 | 
			
		||||
            , placeholder = ""
 | 
			
		||||
            , options = Data.SSLType.all
 | 
			
		||||
            , selected =
 | 
			
		||||
                Data.SSLType.fromString ems.sslType
 | 
			
		||||
                    |> Maybe.withDefault Data.SSLType.None
 | 
			
		||||
                    |> Just
 | 
			
		||||
            }
 | 
			
		||||
    , ignoreCertificates = ems.ignoreCertificates
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
getSettings : Model -> ( Maybe String, EmailSettings )
 | 
			
		||||
getSettings model =
 | 
			
		||||
    ( Util.Maybe.fromString model.settings.name
 | 
			
		||||
    , { name = model.name
 | 
			
		||||
      , smtpHost = model.host
 | 
			
		||||
      , smtpUser = model.user
 | 
			
		||||
      , smtpPort = model.portNum
 | 
			
		||||
      , smtpPassword = model.password
 | 
			
		||||
      , from = model.from
 | 
			
		||||
      , replyTo = model.replyTo
 | 
			
		||||
      , sslType =
 | 
			
		||||
            Comp.Dropdown.getSelected model.sslType
 | 
			
		||||
                |> List.head
 | 
			
		||||
                |> Maybe.withDefault Data.SSLType.None
 | 
			
		||||
                |> Data.SSLType.toString
 | 
			
		||||
      , ignoreCertificates = model.ignoreCertificates
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Msg
 | 
			
		||||
    = SetName String
 | 
			
		||||
    | SetHost String
 | 
			
		||||
    | PortMsg Comp.IntField.Msg
 | 
			
		||||
    | SetUser String
 | 
			
		||||
    | PassMsg Comp.PasswordInput.Msg
 | 
			
		||||
    | SSLTypeMsg (Comp.Dropdown.Msg SSLType)
 | 
			
		||||
    | SetFrom String
 | 
			
		||||
    | SetReplyTo String
 | 
			
		||||
    | ToggleCheckCert
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
isValid : Model -> Bool
 | 
			
		||||
isValid model =
 | 
			
		||||
    model.host /= "" && model.name /= ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
update : Msg -> Model -> ( Model, Cmd Msg )
 | 
			
		||||
update msg model =
 | 
			
		||||
    case msg of
 | 
			
		||||
        SetName str ->
 | 
			
		||||
            ( { model | name = str }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        SetHost str ->
 | 
			
		||||
            ( { model | host = str }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        PortMsg m ->
 | 
			
		||||
            let
 | 
			
		||||
                ( pm, val ) =
 | 
			
		||||
                    Comp.IntField.update m model.portField
 | 
			
		||||
            in
 | 
			
		||||
            ( { model | portField = pm, portNum = val }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        SetUser str ->
 | 
			
		||||
            ( { model | user = Util.Maybe.fromString str }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        PassMsg m ->
 | 
			
		||||
            let
 | 
			
		||||
                ( pm, val ) =
 | 
			
		||||
                    Comp.PasswordInput.update m model.passField
 | 
			
		||||
            in
 | 
			
		||||
            ( { model | passField = pm, password = val }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        SSLTypeMsg m ->
 | 
			
		||||
            let
 | 
			
		||||
                ( sm, sc ) =
 | 
			
		||||
                    Comp.Dropdown.update m model.sslType
 | 
			
		||||
            in
 | 
			
		||||
            ( { model | sslType = sm }, Cmd.map SSLTypeMsg sc )
 | 
			
		||||
 | 
			
		||||
        SetFrom str ->
 | 
			
		||||
            ( { model | from = str }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        SetReplyTo str ->
 | 
			
		||||
            ( { model | replyTo = Util.Maybe.fromString str }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        ToggleCheckCert ->
 | 
			
		||||
            ( { model | ignoreCertificates = not model.ignoreCertificates }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
view : Model -> Html Msg
 | 
			
		||||
view model =
 | 
			
		||||
    div
 | 
			
		||||
        [ classList
 | 
			
		||||
            [ ( "ui form", True )
 | 
			
		||||
            , ( "error", not (isValid model) )
 | 
			
		||||
            , ( "success", isValid model )
 | 
			
		||||
            ]
 | 
			
		||||
        ]
 | 
			
		||||
        [ div [ class "required field" ]
 | 
			
		||||
            [ label [] [ text "Name" ]
 | 
			
		||||
            , input
 | 
			
		||||
                [ type_ "text"
 | 
			
		||||
                , value model.name
 | 
			
		||||
                , onInput SetName
 | 
			
		||||
                , placeholder "Connection name, e.g. 'gmail.com'"
 | 
			
		||||
                ]
 | 
			
		||||
                []
 | 
			
		||||
            , div [ class "ui info message" ]
 | 
			
		||||
                [ text "The connection name must not contain whitespace or special characters."
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
        , div [ class "fields" ]
 | 
			
		||||
            [ div [ class "thirteen wide required field" ]
 | 
			
		||||
                [ label [] [ text "SMTP Host" ]
 | 
			
		||||
                , input
 | 
			
		||||
                    [ type_ "text"
 | 
			
		||||
                    , placeholder "SMTP host name, e.g. 'mail.gmail.com'"
 | 
			
		||||
                    , value model.host
 | 
			
		||||
                    , onInput SetHost
 | 
			
		||||
                    ]
 | 
			
		||||
                    []
 | 
			
		||||
                ]
 | 
			
		||||
            , Html.map PortMsg
 | 
			
		||||
                (Comp.IntField.view model.portNum
 | 
			
		||||
                    "three wide field"
 | 
			
		||||
                    model.portField
 | 
			
		||||
                )
 | 
			
		||||
            ]
 | 
			
		||||
        , div [ class "two fields" ]
 | 
			
		||||
            [ div [ class "field" ]
 | 
			
		||||
                [ label [] [ text "SMTP User" ]
 | 
			
		||||
                , input
 | 
			
		||||
                    [ type_ "text"
 | 
			
		||||
                    , placeholder "SMTP Username, e.g. 'your.name@gmail.com'"
 | 
			
		||||
                    , Maybe.withDefault "" model.user |> value
 | 
			
		||||
                    , onInput SetUser
 | 
			
		||||
                    ]
 | 
			
		||||
                    []
 | 
			
		||||
                ]
 | 
			
		||||
            , div [ class "field" ]
 | 
			
		||||
                [ label [] [ text "SMTP Password" ]
 | 
			
		||||
                , Html.map PassMsg (Comp.PasswordInput.view model.password model.passField)
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
        , div [ class "two fields" ]
 | 
			
		||||
            [ div [ class "required field" ]
 | 
			
		||||
                [ label [] [ text "From Address" ]
 | 
			
		||||
                , input
 | 
			
		||||
                    [ type_ "text"
 | 
			
		||||
                    , placeholder "Sender E-Mail address"
 | 
			
		||||
                    , value model.from
 | 
			
		||||
                    , onInput SetFrom
 | 
			
		||||
                    ]
 | 
			
		||||
                    []
 | 
			
		||||
                ]
 | 
			
		||||
            , div [ class "field" ]
 | 
			
		||||
                [ label [] [ text "Reply-To" ]
 | 
			
		||||
                , input
 | 
			
		||||
                    [ type_ "text"
 | 
			
		||||
                    , placeholder "Optional reply-to E-Mail address"
 | 
			
		||||
                    , Maybe.withDefault "" model.replyTo |> value
 | 
			
		||||
                    , onInput SetReplyTo
 | 
			
		||||
                    ]
 | 
			
		||||
                    []
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
        , div [ class "two fields" ]
 | 
			
		||||
            [ div [ class "inline field" ]
 | 
			
		||||
                [ div [ class "ui checkbox" ]
 | 
			
		||||
                    [ input
 | 
			
		||||
                        [ type_ "checkbox"
 | 
			
		||||
                        , checked model.ignoreCertificates
 | 
			
		||||
                        , onCheck (\_ -> ToggleCheckCert)
 | 
			
		||||
                        ]
 | 
			
		||||
                        []
 | 
			
		||||
                    , label [] [ text "Ignore certificate check" ]
 | 
			
		||||
                    ]
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
        , div [ class "two fields" ]
 | 
			
		||||
            [ div [ class "field" ]
 | 
			
		||||
                [ label [] [ text "SSL" ]
 | 
			
		||||
                , Html.map SSLTypeMsg (Comp.Dropdown.view model.sslType)
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
        ]
 | 
			
		||||
							
								
								
									
										290
									
								
								modules/webapp/src/main/elm/Comp/EmailSettingsManage.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								modules/webapp/src/main/elm/Comp/EmailSettingsManage.elm
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,290 @@
 | 
			
		||||
module Comp.EmailSettingsManage exposing
 | 
			
		||||
    ( Model
 | 
			
		||||
    , Msg
 | 
			
		||||
    , emptyModel
 | 
			
		||||
    , init
 | 
			
		||||
    , update
 | 
			
		||||
    , view
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
import Api
 | 
			
		||||
import Api.Model.BasicResult exposing (BasicResult)
 | 
			
		||||
import Api.Model.EmailSettings exposing (EmailSettings)
 | 
			
		||||
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
 | 
			
		||||
import Comp.EmailSettingsForm
 | 
			
		||||
import Comp.EmailSettingsTable
 | 
			
		||||
import Comp.YesNoDimmer
 | 
			
		||||
import Data.Flags exposing (Flags)
 | 
			
		||||
import Html exposing (..)
 | 
			
		||||
import Html.Attributes exposing (..)
 | 
			
		||||
import Html.Events exposing (onClick, onInput)
 | 
			
		||||
import Http
 | 
			
		||||
import Util.Http
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type alias Model =
 | 
			
		||||
    { tableModel : Comp.EmailSettingsTable.Model
 | 
			
		||||
    , formModel : Comp.EmailSettingsForm.Model
 | 
			
		||||
    , viewMode : ViewMode
 | 
			
		||||
    , formError : Maybe String
 | 
			
		||||
    , loading : Bool
 | 
			
		||||
    , query : String
 | 
			
		||||
    , deleteConfirm : Comp.YesNoDimmer.Model
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
emptyModel : Model
 | 
			
		||||
emptyModel =
 | 
			
		||||
    { tableModel = Comp.EmailSettingsTable.emptyModel
 | 
			
		||||
    , formModel = Comp.EmailSettingsForm.emptyModel
 | 
			
		||||
    , viewMode = Table
 | 
			
		||||
    , formError = Nothing
 | 
			
		||||
    , loading = False
 | 
			
		||||
    , query = ""
 | 
			
		||||
    , deleteConfirm = Comp.YesNoDimmer.emptyModel
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
init : Flags -> ( Model, Cmd Msg )
 | 
			
		||||
init flags =
 | 
			
		||||
    ( emptyModel, Api.getMailSettings flags "" MailSettingsResp )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type ViewMode
 | 
			
		||||
    = Table
 | 
			
		||||
    | Form
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Msg
 | 
			
		||||
    = TableMsg Comp.EmailSettingsTable.Msg
 | 
			
		||||
    | FormMsg Comp.EmailSettingsForm.Msg
 | 
			
		||||
    | SetQuery String
 | 
			
		||||
    | InitNew
 | 
			
		||||
    | YesNoMsg Comp.YesNoDimmer.Msg
 | 
			
		||||
    | RequestDelete
 | 
			
		||||
    | SetViewMode ViewMode
 | 
			
		||||
    | Submit
 | 
			
		||||
    | SubmitResp (Result Http.Error BasicResult)
 | 
			
		||||
    | LoadSettings
 | 
			
		||||
    | MailSettingsResp (Result Http.Error EmailSettingsList)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
 | 
			
		||||
update flags msg model =
 | 
			
		||||
    case msg of
 | 
			
		||||
        InitNew ->
 | 
			
		||||
            let
 | 
			
		||||
                ems =
 | 
			
		||||
                    Api.Model.EmailSettings.empty
 | 
			
		||||
 | 
			
		||||
                nm =
 | 
			
		||||
                    { model
 | 
			
		||||
                        | viewMode = Form
 | 
			
		||||
                        , formError = Nothing
 | 
			
		||||
                        , formModel = Comp.EmailSettingsForm.init ems
 | 
			
		||||
                    }
 | 
			
		||||
            in
 | 
			
		||||
            ( nm, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        TableMsg m ->
 | 
			
		||||
            let
 | 
			
		||||
                ( tm, tc ) =
 | 
			
		||||
                    Comp.EmailSettingsTable.update m model.tableModel
 | 
			
		||||
 | 
			
		||||
                m2 =
 | 
			
		||||
                    { model
 | 
			
		||||
                        | tableModel = tm
 | 
			
		||||
                        , viewMode = Maybe.map (\_ -> Form) tm.selected |> Maybe.withDefault Table
 | 
			
		||||
                        , formError =
 | 
			
		||||
                            if tm.selected /= Nothing then
 | 
			
		||||
                                Nothing
 | 
			
		||||
 | 
			
		||||
                            else
 | 
			
		||||
                                model.formError
 | 
			
		||||
                        , formModel =
 | 
			
		||||
                            case tm.selected of
 | 
			
		||||
                                Just ems ->
 | 
			
		||||
                                    Comp.EmailSettingsForm.init ems
 | 
			
		||||
 | 
			
		||||
                                Nothing ->
 | 
			
		||||
                                    model.formModel
 | 
			
		||||
                    }
 | 
			
		||||
            in
 | 
			
		||||
            ( m2, Cmd.map TableMsg tc )
 | 
			
		||||
 | 
			
		||||
        FormMsg m ->
 | 
			
		||||
            let
 | 
			
		||||
                ( fm, fc ) =
 | 
			
		||||
                    Comp.EmailSettingsForm.update m model.formModel
 | 
			
		||||
            in
 | 
			
		||||
            ( { model | formModel = fm }, Cmd.map FormMsg fc )
 | 
			
		||||
 | 
			
		||||
        SetQuery str ->
 | 
			
		||||
            let
 | 
			
		||||
                m =
 | 
			
		||||
                    { model | query = str }
 | 
			
		||||
            in
 | 
			
		||||
            ( m, Api.getMailSettings flags str MailSettingsResp )
 | 
			
		||||
 | 
			
		||||
        YesNoMsg m ->
 | 
			
		||||
            let
 | 
			
		||||
                ( dm, flag ) =
 | 
			
		||||
                    Comp.YesNoDimmer.update m model.deleteConfirm
 | 
			
		||||
 | 
			
		||||
                ( mid, _ ) =
 | 
			
		||||
                    Comp.EmailSettingsForm.getSettings model.formModel
 | 
			
		||||
 | 
			
		||||
                cmd =
 | 
			
		||||
                    case ( flag, mid ) of
 | 
			
		||||
                        ( True, Just name ) ->
 | 
			
		||||
                            Api.deleteMailSettings flags name SubmitResp
 | 
			
		||||
 | 
			
		||||
                        _ ->
 | 
			
		||||
                            Cmd.none
 | 
			
		||||
            in
 | 
			
		||||
            ( { model | deleteConfirm = dm }, cmd )
 | 
			
		||||
 | 
			
		||||
        RequestDelete ->
 | 
			
		||||
            update flags (YesNoMsg Comp.YesNoDimmer.activate) model
 | 
			
		||||
 | 
			
		||||
        SetViewMode m ->
 | 
			
		||||
            ( { model | viewMode = m }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        Submit ->
 | 
			
		||||
            let
 | 
			
		||||
                ( mid, ems ) =
 | 
			
		||||
                    Comp.EmailSettingsForm.getSettings model.formModel
 | 
			
		||||
 | 
			
		||||
                valid =
 | 
			
		||||
                    Comp.EmailSettingsForm.isValid model.formModel
 | 
			
		||||
            in
 | 
			
		||||
            if valid then
 | 
			
		||||
                ( { model | loading = True }, Api.createMailSettings flags mid ems SubmitResp )
 | 
			
		||||
 | 
			
		||||
            else
 | 
			
		||||
                ( { model | formError = Just "Please fill required fields." }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        LoadSettings ->
 | 
			
		||||
            ( { model | loading = True }, Api.getMailSettings flags model.query MailSettingsResp )
 | 
			
		||||
 | 
			
		||||
        SubmitResp (Ok res) ->
 | 
			
		||||
            if res.success then
 | 
			
		||||
                let
 | 
			
		||||
                    ( m2, c2 ) =
 | 
			
		||||
                        update flags (SetViewMode Table) model
 | 
			
		||||
 | 
			
		||||
                    ( m3, c3 ) =
 | 
			
		||||
                        update flags LoadSettings m2
 | 
			
		||||
                in
 | 
			
		||||
                ( { m3 | loading = False }, Cmd.batch [ c2, c3 ] )
 | 
			
		||||
 | 
			
		||||
            else
 | 
			
		||||
                ( { model | formError = Just res.message, loading = False }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        SubmitResp (Err err) ->
 | 
			
		||||
            ( { model | formError = Just (Util.Http.errorToString err), loading = False }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        MailSettingsResp (Ok ems) ->
 | 
			
		||||
            let
 | 
			
		||||
                m2 =
 | 
			
		||||
                    { model
 | 
			
		||||
                        | viewMode = Table
 | 
			
		||||
                        , loading = False
 | 
			
		||||
                        , tableModel = Comp.EmailSettingsTable.init ems.items
 | 
			
		||||
                    }
 | 
			
		||||
            in
 | 
			
		||||
            ( m2, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        MailSettingsResp (Err _) ->
 | 
			
		||||
            ( { model | loading = False }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
view : Model -> Html Msg
 | 
			
		||||
view model =
 | 
			
		||||
    case model.viewMode of
 | 
			
		||||
        Table ->
 | 
			
		||||
            viewTable model
 | 
			
		||||
 | 
			
		||||
        Form ->
 | 
			
		||||
            viewForm model
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
viewTable : Model -> Html Msg
 | 
			
		||||
viewTable model =
 | 
			
		||||
    div []
 | 
			
		||||
        [ div [ class "ui secondary menu container" ]
 | 
			
		||||
            [ div [ class "ui container" ]
 | 
			
		||||
                [ div [ class "fitted-item" ]
 | 
			
		||||
                    [ div [ class "ui icon input" ]
 | 
			
		||||
                        [ input
 | 
			
		||||
                            [ type_ "text"
 | 
			
		||||
                            , onInput SetQuery
 | 
			
		||||
                            , value model.query
 | 
			
		||||
                            , placeholder "Search…"
 | 
			
		||||
                            ]
 | 
			
		||||
                            []
 | 
			
		||||
                        , i [ class "ui search icon" ]
 | 
			
		||||
                            []
 | 
			
		||||
                        ]
 | 
			
		||||
                    ]
 | 
			
		||||
                , div [ class "right menu" ]
 | 
			
		||||
                    [ div [ class "fitted-item" ]
 | 
			
		||||
                        [ a
 | 
			
		||||
                            [ class "ui primary button"
 | 
			
		||||
                            , href "#"
 | 
			
		||||
                            , onClick InitNew
 | 
			
		||||
                            ]
 | 
			
		||||
                            [ i [ class "plus icon" ] []
 | 
			
		||||
                            , text "New Settings"
 | 
			
		||||
                            ]
 | 
			
		||||
                        ]
 | 
			
		||||
                    ]
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
        , Html.map TableMsg (Comp.EmailSettingsTable.view model.tableModel)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
viewForm : Model -> Html Msg
 | 
			
		||||
viewForm model =
 | 
			
		||||
    div [ class "ui segment" ]
 | 
			
		||||
        [ Html.map YesNoMsg (Comp.YesNoDimmer.view model.deleteConfirm)
 | 
			
		||||
        , Html.map FormMsg (Comp.EmailSettingsForm.view model.formModel)
 | 
			
		||||
        , div
 | 
			
		||||
            [ classList
 | 
			
		||||
                [ ( "ui error message", True )
 | 
			
		||||
                , ( "invisible", model.formError == Nothing )
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
            [ Maybe.withDefault "" model.formError |> text
 | 
			
		||||
            ]
 | 
			
		||||
        , div [ class "ui divider" ] []
 | 
			
		||||
        , button
 | 
			
		||||
            [ class "ui primary button"
 | 
			
		||||
            , onClick Submit
 | 
			
		||||
            , href "#"
 | 
			
		||||
            ]
 | 
			
		||||
            [ text "Submit"
 | 
			
		||||
            ]
 | 
			
		||||
        , a
 | 
			
		||||
            [ class "ui secondary button"
 | 
			
		||||
            , onClick (SetViewMode Table)
 | 
			
		||||
            , href ""
 | 
			
		||||
            ]
 | 
			
		||||
            [ text "Cancel"
 | 
			
		||||
            ]
 | 
			
		||||
        , if model.formModel.settings.name /= "" then
 | 
			
		||||
            a [ class "ui right floated red button", href "", onClick RequestDelete ]
 | 
			
		||||
                [ text "Delete" ]
 | 
			
		||||
 | 
			
		||||
          else
 | 
			
		||||
            span [] []
 | 
			
		||||
        , div
 | 
			
		||||
            [ classList
 | 
			
		||||
                [ ( "ui dimmer", True )
 | 
			
		||||
                , ( "active", model.loading )
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
            [ div [ class "ui loader" ] []
 | 
			
		||||
            ]
 | 
			
		||||
        ]
 | 
			
		||||
							
								
								
									
										76
									
								
								modules/webapp/src/main/elm/Comp/EmailSettingsTable.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								modules/webapp/src/main/elm/Comp/EmailSettingsTable.elm
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
module Comp.EmailSettingsTable exposing
 | 
			
		||||
    ( Model
 | 
			
		||||
    , Msg
 | 
			
		||||
    , emptyModel
 | 
			
		||||
    , init
 | 
			
		||||
    , update
 | 
			
		||||
    , view
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
import Api.Model.EmailSettings exposing (EmailSettings)
 | 
			
		||||
import Html exposing (..)
 | 
			
		||||
import Html.Attributes exposing (..)
 | 
			
		||||
import Html.Events exposing (onClick)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type alias Model =
 | 
			
		||||
    { emailSettings : List EmailSettings
 | 
			
		||||
    , selected : Maybe EmailSettings
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
emptyModel : Model
 | 
			
		||||
emptyModel =
 | 
			
		||||
    init []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
init : List EmailSettings -> Model
 | 
			
		||||
init ems =
 | 
			
		||||
    { emailSettings = ems
 | 
			
		||||
    , selected = Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Msg
 | 
			
		||||
    = Select EmailSettings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
update : Msg -> Model -> ( Model, Cmd Msg )
 | 
			
		||||
update msg model =
 | 
			
		||||
    case msg of
 | 
			
		||||
        Select ems ->
 | 
			
		||||
            ( { model | selected = Just ems }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
view : Model -> Html Msg
 | 
			
		||||
view model =
 | 
			
		||||
    table [ class "ui selectable pointer table" ]
 | 
			
		||||
        [ thead []
 | 
			
		||||
            [ th [ class "collapsible" ] [ text "Name" ]
 | 
			
		||||
            , th [] [ text "Host/Port" ]
 | 
			
		||||
            , th [] [ text "From" ]
 | 
			
		||||
            ]
 | 
			
		||||
        , tbody []
 | 
			
		||||
            (List.map (renderLine model) model.emailSettings)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
renderLine : Model -> EmailSettings -> Html Msg
 | 
			
		||||
renderLine model ems =
 | 
			
		||||
    let
 | 
			
		||||
        hostport =
 | 
			
		||||
            case ems.smtpPort of
 | 
			
		||||
                Just p ->
 | 
			
		||||
                    ems.smtpHost ++ ":" ++ String.fromInt p
 | 
			
		||||
 | 
			
		||||
                Nothing ->
 | 
			
		||||
                    ems.smtpHost
 | 
			
		||||
    in
 | 
			
		||||
    tr
 | 
			
		||||
        [ classList [ ( "active", model.selected == Just ems ) ]
 | 
			
		||||
        , onClick (Select ems)
 | 
			
		||||
        ]
 | 
			
		||||
        [ td [ class "collapsible" ] [ text ems.name ]
 | 
			
		||||
        , td [] [ text hostport ]
 | 
			
		||||
        , td [] [ text ems.from ]
 | 
			
		||||
        ]
 | 
			
		||||
							
								
								
									
										114
									
								
								modules/webapp/src/main/elm/Comp/IntField.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								modules/webapp/src/main/elm/Comp/IntField.elm
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
module Comp.IntField exposing (Model, Msg, init, update, view)
 | 
			
		||||
 | 
			
		||||
import Html exposing (..)
 | 
			
		||||
import Html.Attributes exposing (..)
 | 
			
		||||
import Html.Events exposing (onInput)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type alias Model =
 | 
			
		||||
    { min : Maybe Int
 | 
			
		||||
    , max : Maybe Int
 | 
			
		||||
    , label : String
 | 
			
		||||
    , error : Maybe String
 | 
			
		||||
    , lastInput : String
 | 
			
		||||
    , optional : Bool
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Msg
 | 
			
		||||
    = SetValue String
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
init : Maybe Int -> Maybe Int -> Bool -> String -> Model
 | 
			
		||||
init min max opt label =
 | 
			
		||||
    { min = min
 | 
			
		||||
    , max = max
 | 
			
		||||
    , label = label
 | 
			
		||||
    , error = Nothing
 | 
			
		||||
    , lastInput = ""
 | 
			
		||||
    , optional = opt
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
tooLow : Model -> Int -> Bool
 | 
			
		||||
tooLow model n =
 | 
			
		||||
    Maybe.map ((<) n) model.min
 | 
			
		||||
        |> Maybe.withDefault (not model.optional)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
tooHigh : Model -> Int -> Bool
 | 
			
		||||
tooHigh model n =
 | 
			
		||||
    Maybe.map ((>) n) model.max
 | 
			
		||||
        |> Maybe.withDefault (not model.optional)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
update : Msg -> Model -> ( Model, Maybe Int )
 | 
			
		||||
update msg model =
 | 
			
		||||
    let
 | 
			
		||||
        tooHighError =
 | 
			
		||||
            Maybe.withDefault 0 model.max
 | 
			
		||||
                |> String.fromInt
 | 
			
		||||
                |> (++) "Number must be <= "
 | 
			
		||||
 | 
			
		||||
        tooLowError =
 | 
			
		||||
            Maybe.withDefault 0 model.min
 | 
			
		||||
                |> String.fromInt
 | 
			
		||||
                |> (++) "Number must be >= "
 | 
			
		||||
    in
 | 
			
		||||
    case msg of
 | 
			
		||||
        SetValue str ->
 | 
			
		||||
            let
 | 
			
		||||
                m =
 | 
			
		||||
                    { model | lastInput = str }
 | 
			
		||||
            in
 | 
			
		||||
            case String.toInt str of
 | 
			
		||||
                Just n ->
 | 
			
		||||
                    if tooLow model n then
 | 
			
		||||
                        ( { m | error = Just tooLowError }
 | 
			
		||||
                        , Nothing
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
                    else if tooHigh model n then
 | 
			
		||||
                        ( { m | error = Just tooHighError }
 | 
			
		||||
                        , Nothing
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
                    else
 | 
			
		||||
                        ( { m | error = Nothing }, Just n )
 | 
			
		||||
 | 
			
		||||
                Nothing ->
 | 
			
		||||
                    if model.optional && String.trim str == "" then
 | 
			
		||||
                        ( { m | error = Nothing }, Nothing )
 | 
			
		||||
 | 
			
		||||
                    else
 | 
			
		||||
                        ( { m | error = Just ("'" ++ str ++ "' is not a valid number!") }
 | 
			
		||||
                        , Nothing
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
view : Maybe Int -> String -> Model -> Html Msg
 | 
			
		||||
view nval classes model =
 | 
			
		||||
    div
 | 
			
		||||
        [ classList
 | 
			
		||||
            [ ( classes, True )
 | 
			
		||||
            , ( "error", model.error /= Nothing )
 | 
			
		||||
            ]
 | 
			
		||||
        ]
 | 
			
		||||
        [ label [] [ text model.label ]
 | 
			
		||||
        , input
 | 
			
		||||
            [ type_ "text"
 | 
			
		||||
            , Maybe.map String.fromInt nval
 | 
			
		||||
                |> Maybe.withDefault model.lastInput
 | 
			
		||||
                |> value
 | 
			
		||||
            , onInput SetValue
 | 
			
		||||
            ]
 | 
			
		||||
            []
 | 
			
		||||
        , div
 | 
			
		||||
            [ classList
 | 
			
		||||
                [ ( "ui pointing red basic label", True )
 | 
			
		||||
                , ( "hidden", model.error == Nothing )
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
            [ Maybe.withDefault "" model.error |> text
 | 
			
		||||
            ]
 | 
			
		||||
        ]
 | 
			
		||||
@@ -17,11 +17,14 @@ import Api.Model.OptionalDate exposing (OptionalDate)
 | 
			
		||||
import Api.Model.OptionalId exposing (OptionalId)
 | 
			
		||||
import Api.Model.OptionalText exposing (OptionalText)
 | 
			
		||||
import Api.Model.ReferenceList exposing (ReferenceList)
 | 
			
		||||
import Api.Model.SentMails exposing (SentMails)
 | 
			
		||||
import Api.Model.Tag exposing (Tag)
 | 
			
		||||
import Api.Model.TagList exposing (TagList)
 | 
			
		||||
import Browser.Navigation as Nav
 | 
			
		||||
import Comp.DatePicker
 | 
			
		||||
import Comp.Dropdown exposing (isDropdownChangeMsg)
 | 
			
		||||
import Comp.ItemMail
 | 
			
		||||
import Comp.SentMails
 | 
			
		||||
import Comp.YesNoDimmer
 | 
			
		||||
import Data.Direction exposing (Direction)
 | 
			
		||||
import Data.Flags exposing (Flags)
 | 
			
		||||
@@ -32,6 +35,7 @@ import Html.Events exposing (onClick, onInput)
 | 
			
		||||
import Http
 | 
			
		||||
import Markdown
 | 
			
		||||
import Page exposing (Page(..))
 | 
			
		||||
import Util.Http
 | 
			
		||||
import Util.Maybe
 | 
			
		||||
import Util.Size
 | 
			
		||||
import Util.String
 | 
			
		||||
@@ -57,6 +61,11 @@ type alias Model =
 | 
			
		||||
    , itemProposals : ItemProposals
 | 
			
		||||
    , dueDate : Maybe Int
 | 
			
		||||
    , dueDatePicker : DatePicker
 | 
			
		||||
    , itemMail : Comp.ItemMail.Model
 | 
			
		||||
    , mailOpen : Bool
 | 
			
		||||
    , mailSendResult : Maybe BasicResult
 | 
			
		||||
    , sentMails : Comp.SentMails.Model
 | 
			
		||||
    , sentMailsOpen : Bool
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -116,6 +125,11 @@ emptyModel =
 | 
			
		||||
    , itemProposals = Api.Model.ItemProposals.empty
 | 
			
		||||
    , dueDate = Nothing
 | 
			
		||||
    , dueDatePicker = Comp.DatePicker.emptyModel
 | 
			
		||||
    , itemMail = Comp.ItemMail.emptyModel
 | 
			
		||||
    , mailOpen = False
 | 
			
		||||
    , mailSendResult = Nothing
 | 
			
		||||
    , sentMails = Comp.SentMails.init
 | 
			
		||||
    , sentMailsOpen = False
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -158,6 +172,12 @@ type Msg
 | 
			
		||||
    | GetProposalResp (Result Http.Error ItemProposals)
 | 
			
		||||
    | RemoveDueDate
 | 
			
		||||
    | RemoveDate
 | 
			
		||||
    | ItemMailMsg Comp.ItemMail.Msg
 | 
			
		||||
    | ToggleMail
 | 
			
		||||
    | SendMailResp (Result Http.Error BasicResult)
 | 
			
		||||
    | SentMailsMsg Comp.SentMails.Msg
 | 
			
		||||
    | ToggleSentMails
 | 
			
		||||
    | SentMailsResp (Result Http.Error SentMails)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -258,11 +278,7 @@ setNotes flags model =
 | 
			
		||||
        text =
 | 
			
		||||
            OptionalText model.notesModel
 | 
			
		||||
    in
 | 
			
		||||
    if model.notesModel == Nothing then
 | 
			
		||||
        Cmd.none
 | 
			
		||||
 | 
			
		||||
    else
 | 
			
		||||
        Api.setItemNotes flags model.item.id text SaveResp
 | 
			
		||||
    Api.setItemNotes flags model.item.id text SaveResp
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
setDate : Flags -> Model -> Maybe Int -> Cmd Msg
 | 
			
		||||
@@ -282,12 +298,17 @@ update key flags next msg model =
 | 
			
		||||
            let
 | 
			
		||||
                ( dp, dpc ) =
 | 
			
		||||
                    Comp.DatePicker.init
 | 
			
		||||
 | 
			
		||||
                ( im, ic ) =
 | 
			
		||||
                    Comp.ItemMail.init flags
 | 
			
		||||
            in
 | 
			
		||||
            ( { model | itemDatePicker = dp, dueDatePicker = dp }
 | 
			
		||||
            ( { model | itemDatePicker = dp, dueDatePicker = dp, itemMail = im }
 | 
			
		||||
            , Cmd.batch
 | 
			
		||||
                [ getOptions flags
 | 
			
		||||
                , Cmd.map ItemDatePickerMsg dpc
 | 
			
		||||
                , Cmd.map DueDatePickerMsg dpc
 | 
			
		||||
                , Cmd.map ItemMailMsg ic
 | 
			
		||||
                , Api.getSentMails flags model.item.id SentMailsResp
 | 
			
		||||
                ]
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@@ -366,11 +387,20 @@ update key flags next msg model =
 | 
			
		||||
                , itemDate = item.itemDate
 | 
			
		||||
                , dueDate = item.dueDate
 | 
			
		||||
              }
 | 
			
		||||
            , Cmd.batch [ c1, c2, c3, c4, c5, getOptions flags, proposalCmd ]
 | 
			
		||||
            , Cmd.batch
 | 
			
		||||
                [ c1
 | 
			
		||||
                , c2
 | 
			
		||||
                , c3
 | 
			
		||||
                , c4
 | 
			
		||||
                , c5
 | 
			
		||||
                , getOptions flags
 | 
			
		||||
                , proposalCmd
 | 
			
		||||
                , Api.getSentMails flags item.id SentMailsResp
 | 
			
		||||
                ]
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        SetActiveAttachment pos ->
 | 
			
		||||
            ( { model | visibleAttach = pos }, Cmd.none )
 | 
			
		||||
            ( { model | visibleAttach = pos, sentMailsOpen = False }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        ToggleMenu ->
 | 
			
		||||
            ( { model | menuOpen = not model.menuOpen }, Cmd.none )
 | 
			
		||||
@@ -503,14 +533,7 @@ update key flags next msg model =
 | 
			
		||||
            ( model, setName flags model )
 | 
			
		||||
 | 
			
		||||
        SetNotes str ->
 | 
			
		||||
            ( { model
 | 
			
		||||
                | notesModel =
 | 
			
		||||
                    if str == "" then
 | 
			
		||||
                        Nothing
 | 
			
		||||
 | 
			
		||||
                    else
 | 
			
		||||
                        Just str
 | 
			
		||||
              }
 | 
			
		||||
            ( { model | notesModel = Util.Maybe.fromString str }
 | 
			
		||||
            , Cmd.none
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@@ -690,6 +713,86 @@ update key flags next msg model =
 | 
			
		||||
        GetProposalResp (Err _) ->
 | 
			
		||||
            ( model, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        ItemMailMsg m ->
 | 
			
		||||
            let
 | 
			
		||||
                ( im, fa ) =
 | 
			
		||||
                    Comp.ItemMail.update m model.itemMail
 | 
			
		||||
            in
 | 
			
		||||
            case fa of
 | 
			
		||||
                Comp.ItemMail.FormNone ->
 | 
			
		||||
                    ( { model | itemMail = im }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
                Comp.ItemMail.FormCancel ->
 | 
			
		||||
                    ( { model
 | 
			
		||||
                        | itemMail = Comp.ItemMail.clear im
 | 
			
		||||
                        , mailOpen = False
 | 
			
		||||
                        , mailSendResult = Nothing
 | 
			
		||||
                      }
 | 
			
		||||
                    , Cmd.none
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                Comp.ItemMail.FormSend sm ->
 | 
			
		||||
                    let
 | 
			
		||||
                        mail =
 | 
			
		||||
                            { item = model.item.id
 | 
			
		||||
                            , mail = sm.mail
 | 
			
		||||
                            , conn = sm.conn
 | 
			
		||||
                            }
 | 
			
		||||
                    in
 | 
			
		||||
                    ( model, Api.sendMail flags mail SendMailResp )
 | 
			
		||||
 | 
			
		||||
        ToggleMail ->
 | 
			
		||||
            ( { model | mailOpen = not model.mailOpen }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        SendMailResp (Ok br) ->
 | 
			
		||||
            let
 | 
			
		||||
                mm =
 | 
			
		||||
                    if br.success then
 | 
			
		||||
                        Comp.ItemMail.clear model.itemMail
 | 
			
		||||
 | 
			
		||||
                    else
 | 
			
		||||
                        model.itemMail
 | 
			
		||||
            in
 | 
			
		||||
            ( { model
 | 
			
		||||
                | itemMail = mm
 | 
			
		||||
                , mailSendResult = Just br
 | 
			
		||||
              }
 | 
			
		||||
            , if br.success then
 | 
			
		||||
                Api.itemDetail flags model.item.id GetItemResp
 | 
			
		||||
 | 
			
		||||
              else
 | 
			
		||||
                Cmd.none
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        SendMailResp (Err err) ->
 | 
			
		||||
            let
 | 
			
		||||
                errmsg =
 | 
			
		||||
                    Util.Http.errorToString err
 | 
			
		||||
            in
 | 
			
		||||
            ( { model | mailSendResult = Just (BasicResult False errmsg) }
 | 
			
		||||
            , Cmd.none
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        SentMailsMsg m ->
 | 
			
		||||
            let
 | 
			
		||||
                sm =
 | 
			
		||||
                    Comp.SentMails.update m model.sentMails
 | 
			
		||||
            in
 | 
			
		||||
            ( { model | sentMails = sm }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        ToggleSentMails ->
 | 
			
		||||
            ( { model | sentMailsOpen = not model.sentMailsOpen, visibleAttach = -1 }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        SentMailsResp (Ok list) ->
 | 
			
		||||
            let
 | 
			
		||||
                sm =
 | 
			
		||||
                    Comp.SentMails.initMails list.items
 | 
			
		||||
            in
 | 
			
		||||
            ( { model | sentMails = sm }, Cmd.none )
 | 
			
		||||
 | 
			
		||||
        SentMailsResp (Err err) ->
 | 
			
		||||
            ( model, Cmd.none )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
-- view
 | 
			
		||||
@@ -711,6 +814,7 @@ view inav model =
 | 
			
		||||
        , div
 | 
			
		||||
            [ classList
 | 
			
		||||
                [ ( "ui ablue-comp menu", True )
 | 
			
		||||
                , ( "top attached", model.mailOpen )
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
            [ a [ class "item", Page.href HomePage ]
 | 
			
		||||
@@ -743,13 +847,25 @@ view inav model =
 | 
			
		||||
                    [ ( "toggle item", True )
 | 
			
		||||
                    , ( "active", model.menuOpen )
 | 
			
		||||
                    ]
 | 
			
		||||
                , title "Expand Menu"
 | 
			
		||||
                , title "Edit item"
 | 
			
		||||
                , onClick ToggleMenu
 | 
			
		||||
                , href ""
 | 
			
		||||
                ]
 | 
			
		||||
                [ i [ class "edit icon" ] []
 | 
			
		||||
                ]
 | 
			
		||||
            , a
 | 
			
		||||
                [ classList
 | 
			
		||||
                    [ ( "toggle item", True )
 | 
			
		||||
                    , ( "active", model.mailOpen )
 | 
			
		||||
                    ]
 | 
			
		||||
                , title "Send Mail"
 | 
			
		||||
                , onClick ToggleMail
 | 
			
		||||
                , href "#"
 | 
			
		||||
                ]
 | 
			
		||||
                [ i [ class "mail outline icon" ] []
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
        , renderMailForm model
 | 
			
		||||
        , div [ class "ui grid" ]
 | 
			
		||||
            [ Html.map YesNoMsg (Comp.YesNoDimmer.view model.deleteConfirm)
 | 
			
		||||
            , div
 | 
			
		||||
@@ -827,7 +943,7 @@ renderNotes model =
 | 
			
		||||
                        , onClick ToggleNotes
 | 
			
		||||
                        , href "#"
 | 
			
		||||
                        ]
 | 
			
		||||
                        [ i [ class "delete icon" ] []
 | 
			
		||||
                        [ i [ class "eye slash icon" ] []
 | 
			
		||||
                        ]
 | 
			
		||||
                    ]
 | 
			
		||||
                ]
 | 
			
		||||
@@ -835,6 +951,23 @@ renderNotes model =
 | 
			
		||||
 | 
			
		||||
renderAttachmentsTabMenu : Model -> Html Msg
 | 
			
		||||
renderAttachmentsTabMenu model =
 | 
			
		||||
    let
 | 
			
		||||
        mailTab =
 | 
			
		||||
            if Comp.SentMails.isEmpty model.sentMails then
 | 
			
		||||
                []
 | 
			
		||||
 | 
			
		||||
            else
 | 
			
		||||
                [ div
 | 
			
		||||
                    [ classList
 | 
			
		||||
                        [ ( "right item", True )
 | 
			
		||||
                        , ( "active", model.sentMailsOpen )
 | 
			
		||||
                        ]
 | 
			
		||||
                    , onClick ToggleSentMails
 | 
			
		||||
                    ]
 | 
			
		||||
                    [ text "E-Mails"
 | 
			
		||||
                    ]
 | 
			
		||||
                ]
 | 
			
		||||
    in
 | 
			
		||||
    div [ class "ui top attached tabular menu" ]
 | 
			
		||||
        (List.indexedMap
 | 
			
		||||
            (\pos ->
 | 
			
		||||
@@ -853,11 +986,31 @@ renderAttachmentsTabMenu model =
 | 
			
		||||
                        ]
 | 
			
		||||
            )
 | 
			
		||||
            model.item.attachments
 | 
			
		||||
            ++ mailTab
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
renderAttachmentsTabBody : Model -> List (Html Msg)
 | 
			
		||||
renderAttachmentsTabBody model =
 | 
			
		||||
    let
 | 
			
		||||
        mailTab =
 | 
			
		||||
            if Comp.SentMails.isEmpty model.sentMails then
 | 
			
		||||
                []
 | 
			
		||||
 | 
			
		||||
            else
 | 
			
		||||
                [ div
 | 
			
		||||
                    [ classList
 | 
			
		||||
                        [ ( "ui attached tab segment", True )
 | 
			
		||||
                        , ( "active", model.sentMailsOpen )
 | 
			
		||||
                        ]
 | 
			
		||||
                    ]
 | 
			
		||||
                    [ h3 [ class "ui header" ]
 | 
			
		||||
                        [ text "Sent E-Mails"
 | 
			
		||||
                        ]
 | 
			
		||||
                    , Html.map SentMailsMsg (Comp.SentMails.view model.sentMails)
 | 
			
		||||
                    ]
 | 
			
		||||
                ]
 | 
			
		||||
    in
 | 
			
		||||
    List.indexedMap
 | 
			
		||||
        (\pos ->
 | 
			
		||||
            \a ->
 | 
			
		||||
@@ -874,6 +1027,7 @@ renderAttachmentsTabBody model =
 | 
			
		||||
                    ]
 | 
			
		||||
        )
 | 
			
		||||
        model.item.attachments
 | 
			
		||||
        ++ mailTab
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
renderItemInfo : Model -> Html Msg
 | 
			
		||||
@@ -1197,3 +1351,37 @@ renderDueDateSuggestions model =
 | 
			
		||||
        Util.Time.formatDate
 | 
			
		||||
        (List.take 5 model.itemProposals.dueDate)
 | 
			
		||||
        SetDueDateSuggestion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
renderMailForm : Model -> Html Msg
 | 
			
		||||
renderMailForm model =
 | 
			
		||||
    div
 | 
			
		||||
        [ classList
 | 
			
		||||
            [ ( "ui bottom attached segment", True )
 | 
			
		||||
            , ( "invisible hidden", not model.mailOpen )
 | 
			
		||||
            ]
 | 
			
		||||
        ]
 | 
			
		||||
        [ h4 [ class "ui header" ]
 | 
			
		||||
            [ text "Send this item via E-Mail"
 | 
			
		||||
            ]
 | 
			
		||||
        , Html.map ItemMailMsg (Comp.ItemMail.view model.itemMail)
 | 
			
		||||
        , div
 | 
			
		||||
            [ classList
 | 
			
		||||
                [ ( "ui message", True )
 | 
			
		||||
                , ( "error"
 | 
			
		||||
                  , Maybe.map .success model.mailSendResult
 | 
			
		||||
                        |> Maybe.map not
 | 
			
		||||
                        |> Maybe.withDefault False
 | 
			
		||||
                  )
 | 
			
		||||
                , ( "success"
 | 
			
		||||
                  , Maybe.map .success model.mailSendResult
 | 
			
		||||
                        |> Maybe.withDefault False
 | 
			
		||||
                  )
 | 
			
		||||
                , ( "invisible hidden", model.mailSendResult == Nothing )
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
            [ Maybe.map .message model.mailSendResult
 | 
			
		||||
                |> Maybe.withDefault ""
 | 
			
		||||
                |> text
 | 
			
		||||
            ]
 | 
			
		||||
        ]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										236
									
								
								modules/webapp/src/main/elm/Comp/ItemMail.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								modules/webapp/src/main/elm/Comp/ItemMail.elm
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,236 @@
 | 
			
		||||
module Comp.ItemMail exposing
 | 
			
		||||
    ( FormAction(..)
 | 
			
		||||
    , Model
 | 
			
		||||
    , Msg
 | 
			
		||||
    , clear
 | 
			
		||||
    , emptyModel
 | 
			
		||||
    , init
 | 
			
		||||
    , update
 | 
			
		||||
    , view
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
import Api
 | 
			
		||||
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
 | 
			
		||||
import Api.Model.SimpleMail exposing (SimpleMail)
 | 
			
		||||
import Comp.Dropdown
 | 
			
		||||
import Data.Flags exposing (Flags)
 | 
			
		||||
import Html exposing (..)
 | 
			
		||||
import Html.Attributes exposing (..)
 | 
			
		||||
import Html.Events exposing (onCheck, onClick, onInput)
 | 
			
		||||
import Http
 | 
			
		||||
import Util.Http
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type alias Model =
 | 
			
		||||
    { connectionModel : Comp.Dropdown.Model String
 | 
			
		||||
    , subject : String
 | 
			
		||||
    , receiver : String
 | 
			
		||||
    , body : String
 | 
			
		||||
    , attachAll : Bool
 | 
			
		||||
    , formError : Maybe String
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Msg
 | 
			
		||||
    = SetSubject String
 | 
			
		||||
    | SetReceiver String
 | 
			
		||||
    | SetBody String
 | 
			
		||||
    | ConnMsg (Comp.Dropdown.Msg String)
 | 
			
		||||
    | ConnResp (Result Http.Error EmailSettingsList)
 | 
			
		||||
    | ToggleAttachAll
 | 
			
		||||
    | Cancel
 | 
			
		||||
    | Send
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type alias MailInfo =
 | 
			
		||||
    { conn : String
 | 
			
		||||
    , mail : SimpleMail
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type FormAction
 | 
			
		||||
    = FormSend MailInfo
 | 
			
		||||
    | FormCancel
 | 
			
		||||
    | FormNone
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
emptyModel : Model
 | 
			
		||||
emptyModel =
 | 
			
		||||
    { connectionModel =
 | 
			
		||||
        Comp.Dropdown.makeSingle
 | 
			
		||||
            { makeOption = \a -> { value = a, text = a }
 | 
			
		||||
            , placeholder = "Select connection..."
 | 
			
		||||
            }
 | 
			
		||||
    , subject = ""
 | 
			
		||||
    , receiver = ""
 | 
			
		||||
    , body = ""
 | 
			
		||||
    , attachAll = True
 | 
			
		||||
    , formError = Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
init : Flags -> ( Model, Cmd Msg )
 | 
			
		||||
init flags =
 | 
			
		||||
    ( emptyModel, Api.getMailSettings flags "" ConnResp )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
clear : Model -> Model
 | 
			
		||||
clear model =
 | 
			
		||||
    { model
 | 
			
		||||
        | subject = ""
 | 
			
		||||
        , receiver = ""
 | 
			
		||||
        , body = ""
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
update : Msg -> Model -> ( Model, FormAction )
 | 
			
		||||
update msg model =
 | 
			
		||||
    case msg of
 | 
			
		||||
        SetSubject str ->
 | 
			
		||||
            ( { model | subject = str }, FormNone )
 | 
			
		||||
 | 
			
		||||
        SetReceiver str ->
 | 
			
		||||
            ( { model | receiver = str }, FormNone )
 | 
			
		||||
 | 
			
		||||
        SetBody str ->
 | 
			
		||||
            ( { model | body = str }, FormNone )
 | 
			
		||||
 | 
			
		||||
        ConnMsg m ->
 | 
			
		||||
            let
 | 
			
		||||
                ( cm, _ ) =
 | 
			
		||||
                    --TODO dropdown doesn't use cmd!!
 | 
			
		||||
                    Comp.Dropdown.update m model.connectionModel
 | 
			
		||||
            in
 | 
			
		||||
            ( { model | connectionModel = cm }, FormNone )
 | 
			
		||||
 | 
			
		||||
        ToggleAttachAll ->
 | 
			
		||||
            ( { model | attachAll = not model.attachAll }, FormNone )
 | 
			
		||||
 | 
			
		||||
        ConnResp (Ok list) ->
 | 
			
		||||
            let
 | 
			
		||||
                names =
 | 
			
		||||
                    List.map .name list.items
 | 
			
		||||
 | 
			
		||||
                cm =
 | 
			
		||||
                    Comp.Dropdown.makeSingleList
 | 
			
		||||
                        { makeOption = \a -> { value = a, text = a }
 | 
			
		||||
                        , placeholder = "Select Connection..."
 | 
			
		||||
                        , options = names
 | 
			
		||||
                        , selected = List.head names
 | 
			
		||||
                        }
 | 
			
		||||
            in
 | 
			
		||||
            ( { model
 | 
			
		||||
                | connectionModel = cm
 | 
			
		||||
                , formError =
 | 
			
		||||
                    if names == [] then
 | 
			
		||||
                        Just "No E-Mail connections configured. Goto user settings to add one."
 | 
			
		||||
 | 
			
		||||
                    else
 | 
			
		||||
                        Nothing
 | 
			
		||||
              }
 | 
			
		||||
            , FormNone
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        ConnResp (Err err) ->
 | 
			
		||||
            ( { model | formError = Just (Util.Http.errorToString err) }, FormNone )
 | 
			
		||||
 | 
			
		||||
        Cancel ->
 | 
			
		||||
            ( model, FormCancel )
 | 
			
		||||
 | 
			
		||||
        Send ->
 | 
			
		||||
            case ( model.formError, Comp.Dropdown.getSelected model.connectionModel ) of
 | 
			
		||||
                ( Nothing, conn :: [] ) ->
 | 
			
		||||
                    let
 | 
			
		||||
                        rec =
 | 
			
		||||
                            String.split "," model.receiver
 | 
			
		||||
 | 
			
		||||
                        sm =
 | 
			
		||||
                            SimpleMail rec model.subject model.body model.attachAll []
 | 
			
		||||
                    in
 | 
			
		||||
                    ( model, FormSend { conn = conn, mail = sm } )
 | 
			
		||||
 | 
			
		||||
                _ ->
 | 
			
		||||
                    ( model, FormNone )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
isValid : Model -> Bool
 | 
			
		||||
isValid model =
 | 
			
		||||
    model.receiver
 | 
			
		||||
        /= ""
 | 
			
		||||
        && model.subject
 | 
			
		||||
        /= ""
 | 
			
		||||
        && model.body
 | 
			
		||||
        /= ""
 | 
			
		||||
        && model.formError
 | 
			
		||||
        == Nothing
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
view : Model -> Html Msg
 | 
			
		||||
view model =
 | 
			
		||||
    div
 | 
			
		||||
        [ classList
 | 
			
		||||
            [ ( "ui form", True )
 | 
			
		||||
            , ( "error", model.formError /= Nothing )
 | 
			
		||||
            ]
 | 
			
		||||
        ]
 | 
			
		||||
        [ div [ class "field" ]
 | 
			
		||||
            [ label [] [ text "Send via" ]
 | 
			
		||||
            , Html.map ConnMsg (Comp.Dropdown.view model.connectionModel)
 | 
			
		||||
            ]
 | 
			
		||||
        , div [ class "ui error message" ]
 | 
			
		||||
            [ Maybe.withDefault "" model.formError |> text
 | 
			
		||||
            ]
 | 
			
		||||
        , div [ class "field" ]
 | 
			
		||||
            [ label []
 | 
			
		||||
                [ text "Receiver(s)"
 | 
			
		||||
                , span [ class "muted" ]
 | 
			
		||||
                    [ text "Separate multiple recipients by comma" ]
 | 
			
		||||
                ]
 | 
			
		||||
            , input
 | 
			
		||||
                [ type_ "text"
 | 
			
		||||
                , onInput SetReceiver
 | 
			
		||||
                , value model.receiver
 | 
			
		||||
                ]
 | 
			
		||||
                []
 | 
			
		||||
            ]
 | 
			
		||||
        , div [ class "field" ]
 | 
			
		||||
            [ label [] [ text "Subject" ]
 | 
			
		||||
            , input
 | 
			
		||||
                [ type_ "text"
 | 
			
		||||
                , onInput SetSubject
 | 
			
		||||
                , value model.subject
 | 
			
		||||
                ]
 | 
			
		||||
                []
 | 
			
		||||
            ]
 | 
			
		||||
        , div [ class "field" ]
 | 
			
		||||
            [ label [] [ text "Body" ]
 | 
			
		||||
            , textarea [ onInput SetBody ]
 | 
			
		||||
                [ text model.body ]
 | 
			
		||||
            ]
 | 
			
		||||
        , div [ class "inline field" ]
 | 
			
		||||
            [ div [ class "ui checkbox" ]
 | 
			
		||||
                [ input
 | 
			
		||||
                    [ type_ "checkbox"
 | 
			
		||||
                    , checked model.attachAll
 | 
			
		||||
                    , onCheck (\_ -> ToggleAttachAll)
 | 
			
		||||
                    ]
 | 
			
		||||
                    []
 | 
			
		||||
                , label [] [ text "Include all item attachments" ]
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
        , button
 | 
			
		||||
            [ classList
 | 
			
		||||
                [ ( "ui primary button", True )
 | 
			
		||||
                , ( "disabled", not (isValid model) )
 | 
			
		||||
                ]
 | 
			
		||||
            , onClick Send
 | 
			
		||||
            ]
 | 
			
		||||
            [ text "Send"
 | 
			
		||||
            ]
 | 
			
		||||
        , button
 | 
			
		||||
            [ class "ui secondary button"
 | 
			
		||||
            , onClick Cancel
 | 
			
		||||
            ]
 | 
			
		||||
            [ text "Cancel"
 | 
			
		||||
            ]
 | 
			
		||||
        ]
 | 
			
		||||
							
								
								
									
										74
									
								
								modules/webapp/src/main/elm/Comp/PasswordInput.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								modules/webapp/src/main/elm/Comp/PasswordInput.elm
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
module Comp.PasswordInput exposing
 | 
			
		||||
    ( Model
 | 
			
		||||
    , Msg
 | 
			
		||||
    , init
 | 
			
		||||
    , update
 | 
			
		||||
    , view
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
import Html exposing (..)
 | 
			
		||||
import Html.Attributes exposing (..)
 | 
			
		||||
import Html.Events exposing (onClick, onInput)
 | 
			
		||||
import Util.Maybe
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type alias Model =
 | 
			
		||||
    { show : Bool
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
init : Model
 | 
			
		||||
init =
 | 
			
		||||
    { show = False
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Msg
 | 
			
		||||
    = ToggleShow (Maybe String)
 | 
			
		||||
    | SetPassword String
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
update : Msg -> Model -> ( Model, Maybe String )
 | 
			
		||||
update msg model =
 | 
			
		||||
    case msg of
 | 
			
		||||
        ToggleShow pw ->
 | 
			
		||||
            ( { model | show = not model.show }
 | 
			
		||||
            , pw
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        SetPassword str ->
 | 
			
		||||
            let
 | 
			
		||||
                pw =
 | 
			
		||||
                    Util.Maybe.fromString str
 | 
			
		||||
            in
 | 
			
		||||
            ( model, pw )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
view : Maybe String -> Model -> Html Msg
 | 
			
		||||
view pw model =
 | 
			
		||||
    div [ class "ui  left action input" ]
 | 
			
		||||
        [ button
 | 
			
		||||
            [ class "ui icon button"
 | 
			
		||||
            , type_ "button"
 | 
			
		||||
            , onClick (ToggleShow pw)
 | 
			
		||||
            ]
 | 
			
		||||
            [ i
 | 
			
		||||
                [ classList
 | 
			
		||||
                    [ ( "ui eye icon", True )
 | 
			
		||||
                    , ( "slash", model.show )
 | 
			
		||||
                    ]
 | 
			
		||||
                ]
 | 
			
		||||
                []
 | 
			
		||||
            ]
 | 
			
		||||
        , input
 | 
			
		||||
            [ type_ <|
 | 
			
		||||
                if model.show then
 | 
			
		||||
                    "text"
 | 
			
		||||
 | 
			
		||||
                else
 | 
			
		||||
                    "password"
 | 
			
		||||
            , onInput SetPassword
 | 
			
		||||
            , Maybe.withDefault "" pw |> value
 | 
			
		||||
            ]
 | 
			
		||||
            []
 | 
			
		||||
        ]
 | 
			
		||||
							
								
								
									
										121
									
								
								modules/webapp/src/main/elm/Comp/SentMails.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								modules/webapp/src/main/elm/Comp/SentMails.elm
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
module Comp.SentMails exposing (..)
 | 
			
		||||
 | 
			
		||||
import Api.Model.SentMail exposing (SentMail)
 | 
			
		||||
import Html exposing (..)
 | 
			
		||||
import Html.Attributes exposing (..)
 | 
			
		||||
import Html.Events exposing (onClick)
 | 
			
		||||
import Util.Time
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type alias Model =
 | 
			
		||||
    { mails : List SentMail
 | 
			
		||||
    , selected : Maybe SentMail
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
init : Model
 | 
			
		||||
init =
 | 
			
		||||
    { mails = []
 | 
			
		||||
    , selected = Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
initMails : List SentMail -> Model
 | 
			
		||||
initMails mails =
 | 
			
		||||
    { init | mails = mails }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
isEmpty : Model -> Bool
 | 
			
		||||
isEmpty model =
 | 
			
		||||
    List.isEmpty model.mails
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Msg
 | 
			
		||||
    = Show SentMail
 | 
			
		||||
    | Hide
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
update : Msg -> Model -> Model
 | 
			
		||||
update msg model =
 | 
			
		||||
    case msg of
 | 
			
		||||
        Hide ->
 | 
			
		||||
            { model | selected = Nothing }
 | 
			
		||||
 | 
			
		||||
        Show m ->
 | 
			
		||||
            { model | selected = Just m }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
view : Model -> Html Msg
 | 
			
		||||
view model =
 | 
			
		||||
    case model.selected of
 | 
			
		||||
        Just mail ->
 | 
			
		||||
            div [ class "ui blue basic segment" ]
 | 
			
		||||
                [ div [ class "ui list" ]
 | 
			
		||||
                    [ div [ class "item" ]
 | 
			
		||||
                        [ text "From"
 | 
			
		||||
                        , div [ class "header" ]
 | 
			
		||||
                            [ text mail.sender
 | 
			
		||||
                            , text " ("
 | 
			
		||||
                            , text mail.connection
 | 
			
		||||
                            , text ")"
 | 
			
		||||
                            ]
 | 
			
		||||
                        ]
 | 
			
		||||
                    , div [ class "item" ]
 | 
			
		||||
                        [ text "Date"
 | 
			
		||||
                        , div [ class "header" ]
 | 
			
		||||
                            [ Util.Time.formatDateTime mail.created |> text
 | 
			
		||||
                            ]
 | 
			
		||||
                        ]
 | 
			
		||||
                    , div [ class "item" ]
 | 
			
		||||
                        [ text "Recipients"
 | 
			
		||||
                        , div [ class "header" ]
 | 
			
		||||
                            [ String.join ", " mail.recipients |> text
 | 
			
		||||
                            ]
 | 
			
		||||
                        ]
 | 
			
		||||
                    , div [ class "item" ]
 | 
			
		||||
                        [ text "Subject"
 | 
			
		||||
                        , div [ class "header" ]
 | 
			
		||||
                            [ text mail.subject
 | 
			
		||||
                            ]
 | 
			
		||||
                        ]
 | 
			
		||||
                    ]
 | 
			
		||||
                , div [ class "ui horizontal divider" ] []
 | 
			
		||||
                , div [ class "mail-body" ]
 | 
			
		||||
                    [ text mail.body
 | 
			
		||||
                    ]
 | 
			
		||||
                , a
 | 
			
		||||
                    [ class "ui right corner label"
 | 
			
		||||
                    , onClick Hide
 | 
			
		||||
                    , href "#"
 | 
			
		||||
                    ]
 | 
			
		||||
                    [ i [ class "close icon" ] []
 | 
			
		||||
                    ]
 | 
			
		||||
                ]
 | 
			
		||||
 | 
			
		||||
        Nothing ->
 | 
			
		||||
            table [ class "ui selectable pointer very basic table" ]
 | 
			
		||||
                [ thead []
 | 
			
		||||
                    [ th [ class "collapsing" ] [ text "Recipients" ]
 | 
			
		||||
                    , th [] [ text "Subject" ]
 | 
			
		||||
                    , th [ class "collapsible" ] [ text "Sent" ]
 | 
			
		||||
                    , th [ class "collapsible" ] [ text "Sender" ]
 | 
			
		||||
                    ]
 | 
			
		||||
                , tbody [] <|
 | 
			
		||||
                    List.map
 | 
			
		||||
                        renderLine
 | 
			
		||||
                        model.mails
 | 
			
		||||
                ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
renderLine : SentMail -> Html Msg
 | 
			
		||||
renderLine mail =
 | 
			
		||||
    tr [ onClick (Show mail) ]
 | 
			
		||||
        [ td [ class "collapsing" ]
 | 
			
		||||
            [ String.join ", " mail.recipients |> text
 | 
			
		||||
            ]
 | 
			
		||||
        , td [] [ text mail.subject ]
 | 
			
		||||
        , td [ class "collapsing" ]
 | 
			
		||||
            [ Util.Time.formatDateTime mail.created |> text
 | 
			
		||||
            ]
 | 
			
		||||
        , td [ class "collapsing" ] [ text mail.sender ]
 | 
			
		||||
        ]
 | 
			
		||||
							
								
								
									
										60
									
								
								modules/webapp/src/main/elm/Data/SSLType.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								modules/webapp/src/main/elm/Data/SSLType.elm
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
module Data.SSLType exposing
 | 
			
		||||
    ( SSLType(..)
 | 
			
		||||
    , all
 | 
			
		||||
    , fromString
 | 
			
		||||
    , label
 | 
			
		||||
    , toString
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type SSLType
 | 
			
		||||
    = None
 | 
			
		||||
    | SSL
 | 
			
		||||
    | StartTLS
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
all : List SSLType
 | 
			
		||||
all =
 | 
			
		||||
    [ None, SSL, StartTLS ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
toString : SSLType -> String
 | 
			
		||||
toString st =
 | 
			
		||||
    case st of
 | 
			
		||||
        None ->
 | 
			
		||||
            "none"
 | 
			
		||||
 | 
			
		||||
        SSL ->
 | 
			
		||||
            "ssl"
 | 
			
		||||
 | 
			
		||||
        StartTLS ->
 | 
			
		||||
            "starttls"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
fromString : String -> Maybe SSLType
 | 
			
		||||
fromString str =
 | 
			
		||||
    case String.toLower str of
 | 
			
		||||
        "none" ->
 | 
			
		||||
            Just None
 | 
			
		||||
 | 
			
		||||
        "ssl" ->
 | 
			
		||||
            Just SSL
 | 
			
		||||
 | 
			
		||||
        "starttls" ->
 | 
			
		||||
            Just StartTLS
 | 
			
		||||
 | 
			
		||||
        _ ->
 | 
			
		||||
            Nothing
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
label : SSLType -> String
 | 
			
		||||
label st =
 | 
			
		||||
    case st of
 | 
			
		||||
        None ->
 | 
			
		||||
            "None"
 | 
			
		||||
 | 
			
		||||
        SSL ->
 | 
			
		||||
            "SSL/TLS"
 | 
			
		||||
 | 
			
		||||
        StartTLS ->
 | 
			
		||||
            "StartTLS"
 | 
			
		||||
@@ -6,11 +6,13 @@ module Page.UserSettings.Data exposing
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
import Comp.ChangePasswordForm
 | 
			
		||||
import Comp.EmailSettingsManage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type alias Model =
 | 
			
		||||
    { currentTab : Maybe Tab
 | 
			
		||||
    , changePassModel : Comp.ChangePasswordForm.Model
 | 
			
		||||
    , emailSettingsModel : Comp.EmailSettingsManage.Model
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -18,13 +20,16 @@ emptyModel : Model
 | 
			
		||||
emptyModel =
 | 
			
		||||
    { currentTab = Nothing
 | 
			
		||||
    , changePassModel = Comp.ChangePasswordForm.emptyModel
 | 
			
		||||
    , emailSettingsModel = Comp.EmailSettingsManage.emptyModel
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Tab
 | 
			
		||||
    = ChangePassTab
 | 
			
		||||
    | EmailSettingsTab
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Msg
 | 
			
		||||
    = SetTab Tab
 | 
			
		||||
    | ChangePassMsg Comp.ChangePasswordForm.Msg
 | 
			
		||||
    | EmailSettingsMsg Comp.EmailSettingsManage.Msg
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
module Page.UserSettings.Update exposing (update)
 | 
			
		||||
 | 
			
		||||
import Comp.ChangePasswordForm
 | 
			
		||||
import Comp.EmailSettingsManage
 | 
			
		||||
import Data.Flags exposing (Flags)
 | 
			
		||||
import Page.UserSettings.Data exposing (..)
 | 
			
		||||
 | 
			
		||||
@@ -12,8 +13,20 @@ update flags msg model =
 | 
			
		||||
            let
 | 
			
		||||
                m =
 | 
			
		||||
                    { model | currentTab = Just t }
 | 
			
		||||
 | 
			
		||||
                ( m2, cmd ) =
 | 
			
		||||
                    case t of
 | 
			
		||||
                        EmailSettingsTab ->
 | 
			
		||||
                            let
 | 
			
		||||
                                ( em, c ) =
 | 
			
		||||
                                    Comp.EmailSettingsManage.init flags
 | 
			
		||||
                            in
 | 
			
		||||
                            ( { m | emailSettingsModel = em }, Cmd.map EmailSettingsMsg c )
 | 
			
		||||
 | 
			
		||||
                        ChangePassTab ->
 | 
			
		||||
                            ( m, Cmd.none )
 | 
			
		||||
            in
 | 
			
		||||
            ( m, Cmd.none )
 | 
			
		||||
            ( m2, cmd )
 | 
			
		||||
 | 
			
		||||
        ChangePassMsg m ->
 | 
			
		||||
            let
 | 
			
		||||
@@ -21,3 +34,10 @@ update flags msg model =
 | 
			
		||||
                    Comp.ChangePasswordForm.update flags m model.changePassModel
 | 
			
		||||
            in
 | 
			
		||||
            ( { model | changePassModel = m2 }, Cmd.map ChangePassMsg c2 )
 | 
			
		||||
 | 
			
		||||
        EmailSettingsMsg m ->
 | 
			
		||||
            let
 | 
			
		||||
                ( m2, c2 ) =
 | 
			
		||||
                    Comp.EmailSettingsManage.update flags m model.emailSettingsModel
 | 
			
		||||
            in
 | 
			
		||||
            ( { model | emailSettingsModel = m2 }, Cmd.map EmailSettingsMsg c2 )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
module Page.UserSettings.View exposing (view)
 | 
			
		||||
 | 
			
		||||
import Comp.ChangePasswordForm
 | 
			
		||||
import Comp.EmailSettingsManage
 | 
			
		||||
import Html exposing (..)
 | 
			
		||||
import Html.Attributes exposing (..)
 | 
			
		||||
import Html.Events exposing (onClick)
 | 
			
		||||
@@ -17,13 +18,22 @@ view model =
 | 
			
		||||
                ]
 | 
			
		||||
            , div [ class "ui attached fluid segment" ]
 | 
			
		||||
                [ div [ class "ui fluid vertical secondary menu" ]
 | 
			
		||||
                    [ div
 | 
			
		||||
                    [ a
 | 
			
		||||
                        [ classActive (model.currentTab == Just ChangePassTab) "link icon item"
 | 
			
		||||
                        , onClick (SetTab ChangePassTab)
 | 
			
		||||
                        , href "#"
 | 
			
		||||
                        ]
 | 
			
		||||
                        [ i [ class "user secret icon" ] []
 | 
			
		||||
                        , text "Change Password"
 | 
			
		||||
                        ]
 | 
			
		||||
                    , a
 | 
			
		||||
                        [ classActive (model.currentTab == Just EmailSettingsTab) "link icon item"
 | 
			
		||||
                        , onClick (SetTab EmailSettingsTab)
 | 
			
		||||
                        , href "#"
 | 
			
		||||
                        ]
 | 
			
		||||
                        [ i [ class "mail icon" ] []
 | 
			
		||||
                        , text "E-Mail Settings"
 | 
			
		||||
                        ]
 | 
			
		||||
                    ]
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
@@ -33,6 +43,9 @@ view model =
 | 
			
		||||
                    Just ChangePassTab ->
 | 
			
		||||
                        viewChangePassword model
 | 
			
		||||
 | 
			
		||||
                    Just EmailSettingsTab ->
 | 
			
		||||
                        viewEmailSettings model
 | 
			
		||||
 | 
			
		||||
                    Nothing ->
 | 
			
		||||
                        []
 | 
			
		||||
                )
 | 
			
		||||
@@ -40,6 +53,18 @@ view model =
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
viewEmailSettings : Model -> List (Html Msg)
 | 
			
		||||
viewEmailSettings model =
 | 
			
		||||
    [ h2 [ class "ui header" ]
 | 
			
		||||
        [ i [ class "mail icon" ] []
 | 
			
		||||
        , div [ class "content" ]
 | 
			
		||||
            [ text "E-Mail Settings"
 | 
			
		||||
            ]
 | 
			
		||||
        ]
 | 
			
		||||
    , Html.map EmailSettingsMsg (Comp.EmailSettingsManage.view model.emailSettingsModel)
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
viewChangePassword : Model -> List (Html Msg)
 | 
			
		||||
viewChangePassword model =
 | 
			
		||||
    [ h2 [ class "ui header" ]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
module Util.Maybe exposing
 | 
			
		||||
    ( isEmpty
 | 
			
		||||
    ( fromString
 | 
			
		||||
    , isEmpty
 | 
			
		||||
    , nonEmpty
 | 
			
		||||
    , or
 | 
			
		||||
    , withDefault
 | 
			
		||||
@@ -38,3 +39,16 @@ or listma =
 | 
			
		||||
 | 
			
		||||
                Nothing ->
 | 
			
		||||
                    or els
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
fromString : String -> Maybe String
 | 
			
		||||
fromString str =
 | 
			
		||||
    let
 | 
			
		||||
        s =
 | 
			
		||||
            String.trim str
 | 
			
		||||
    in
 | 
			
		||||
    if s == "" then
 | 
			
		||||
        Nothing
 | 
			
		||||
 | 
			
		||||
    else
 | 
			
		||||
        Just str
 | 
			
		||||
 
 | 
			
		||||
@@ -65,6 +65,12 @@
 | 
			
		||||
    padding-right: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
label span.muted {
 | 
			
		||||
    font-size: smaller;
 | 
			
		||||
    color: rgba(0,0,0,0.6);
 | 
			
		||||
    margin-left: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ui.search.dropdown.open {
 | 
			
		||||
    z-index: 20;
 | 
			
		||||
}
 | 
			
		||||
@@ -88,6 +94,10 @@
 | 
			
		||||
    background-color: #d8dfe5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ui.selectable.pointer.table tr {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
span.small-info {
 | 
			
		||||
    font-size: smaller;
 | 
			
		||||
    color: rgba(0,0,0,0.6);
 | 
			
		||||
@@ -97,6 +107,10 @@ span.small-info {
 | 
			
		||||
    color: rgba(0,0,0,0.4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mail-body {
 | 
			
		||||
    white-space: pre;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.login-layout, .register-layout, .newinvite-layout {
 | 
			
		||||
    background: #708090;
 | 
			
		||||
    height: 101vh;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ object Dependencies {
 | 
			
		||||
  val BitpeaceVersion = "0.4.2"
 | 
			
		||||
  val CirceVersion = "0.12.3"
 | 
			
		||||
  val DoobieVersion = "0.8.8"
 | 
			
		||||
  val EmilVersion = "0.2.0"
 | 
			
		||||
  val FastparseVersion = "2.1.3"
 | 
			
		||||
  val FlywayVersion = "6.1.4"
 | 
			
		||||
  val Fs2Version = "2.1.0"
 | 
			
		||||
@@ -26,6 +27,11 @@ object Dependencies {
 | 
			
		||||
  val TikaVersion = "1.23"
 | 
			
		||||
  val YamuscaVersion = "0.6.1"
 | 
			
		||||
 | 
			
		||||
  val emil = Seq(
 | 
			
		||||
    "com.github.eikek" %% "emil-common" % EmilVersion,
 | 
			
		||||
    "com.github.eikek" %% "emil-javamail" % EmilVersion    
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  val stanfordNlpCore = Seq(
 | 
			
		||||
    "edu.stanford.nlp" % "stanford-corenlp" % StanfordNlpVersion excludeAll(
 | 
			
		||||
      ExclusionRule("com.io7m.xom", "xom"),
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user