mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-03-28 09:45:07 +00:00
commit
36075fbaaf
@ -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"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user