mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-03-28 09:45:07 +00:00
Starting with mail functionality
This commit is contained in:
parent
2e3454c7a1
commit
f235f3a030
@ -23,6 +23,7 @@ trait BackendApp[F[_]] {
|
||||
def node: ONode[F]
|
||||
def job: OJob[F]
|
||||
def item: OItem[F]
|
||||
def mail: OMail[F]
|
||||
}
|
||||
|
||||
object BackendApp {
|
||||
@ -45,6 +46,7 @@ object BackendApp {
|
||||
nodeImpl <- ONode(store)
|
||||
jobImpl <- OJob(store, httpClientEc)
|
||||
itemImpl <- OItem(store)
|
||||
mailImpl <- OMail(store)
|
||||
} yield new BackendApp[F] {
|
||||
val login: Login[F] = loginImpl
|
||||
val signup: OSignup[F] = signupImpl
|
||||
@ -57,6 +59,7 @@ object BackendApp {
|
||||
val node = nodeImpl
|
||||
val job = jobImpl
|
||||
val item = itemImpl
|
||||
val mail = mailImpl
|
||||
}
|
||||
|
||||
def apply[F[_]: ConcurrentEffect: ContextShift](
|
||||
|
@ -0,0 +1,43 @@
|
||||
package docspell.backend.ops
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import cats.data.OptionT
|
||||
import docspell.common._
|
||||
import docspell.store._
|
||||
import docspell.store.records.RUserEmail
|
||||
|
||||
trait OMail[F[_]] {
|
||||
|
||||
def getSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]]
|
||||
|
||||
def createSettings(data: F[RUserEmail]): F[AddResult]
|
||||
|
||||
def updateSettings(accId: AccountId, name: Ident, data: RUserEmail): F[Int]
|
||||
}
|
||||
|
||||
object OMail {
|
||||
|
||||
def apply[F[_]: Effect](store: Store[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 createSettings(data: F[RUserEmail]): F[AddResult] =
|
||||
for {
|
||||
ru <- data
|
||||
ins = RUserEmail.insert(ru)
|
||||
exists = RUserEmail.exists(ru.uid, ru.name)
|
||||
ar <- store.add(ins, exists)
|
||||
} yield ar
|
||||
|
||||
def updateSettings(accId: AccountId, name: Ident, data: RUserEmail): F[Int] = {
|
||||
val op = for {
|
||||
um <- OptionT(RUserEmail.getByName(accId, name))
|
||||
n <- OptionT.liftF(RUserEmail.update(um.id, data))
|
||||
} yield n
|
||||
|
||||
store.transact(op.value).map(_.getOrElse(0))
|
||||
}
|
||||
})
|
||||
}
|
@ -1222,7 +1222,10 @@ paths:
|
||||
sent e-mails.
|
||||
|
||||
Multiple e-mail settings can be specified, they are
|
||||
distinguished by their `name`.
|
||||
distinguished by their `name`. The query `q` parameter does a
|
||||
simple substring search in the connection name.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/q"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
@ -1352,12 +1355,9 @@ components:
|
||||
required:
|
||||
- name
|
||||
- smtpHost
|
||||
- smtpPort
|
||||
- smtpUser
|
||||
- smtpPassword
|
||||
- from
|
||||
- sslType
|
||||
- certificateCheck
|
||||
- ignoreCertificates
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
@ -1374,9 +1374,11 @@ components:
|
||||
format: password
|
||||
from:
|
||||
type: string
|
||||
replyTo:
|
||||
type: string
|
||||
sslType:
|
||||
type: string
|
||||
certificateCheck:
|
||||
ignoreCertificates:
|
||||
type: boolean
|
||||
CheckFileResult:
|
||||
description: |
|
||||
|
@ -57,19 +57,20 @@ 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/settings" -> MailSettingsRoutes(restApp.backend, token)
|
||||
)
|
||||
|
||||
def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||
|
@ -0,0 +1,53 @@
|
||||
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.common._
|
||||
import docspell.restapi.model._
|
||||
import docspell.store.records.RUserEmail
|
||||
import docspell.store.EmilUtil
|
||||
|
||||
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 req @ POST -> Root =>
|
||||
for {
|
||||
in <- req.as[EmailSettings]
|
||||
resp <- Ok(BasicResult(false, "not implemented"))
|
||||
} 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
|
||||
)
|
||||
}
|
34
modules/store/src/main/scala/docspell/store/EmilUtil.scala
Normal file
34
modules/store/src/main/scala/docspell/store/EmilUtil.scala
Normal file
@ -0,0 +1,34 @@
|
||||
package docspell.store
|
||||
|
||||
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 mailAddressString(ma: MailAddress): String =
|
||||
ma.asUnicodeString
|
||||
}
|
@ -2,18 +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 docspell.common.syntax.all._
|
||||
import emil.{MailAddress, SSLType}
|
||||
import emil.javamail.syntax._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.store.EmilUtil
|
||||
|
||||
trait DoobieMeta {
|
||||
|
||||
@ -92,29 +89,14 @@ trait DoobieMeta {
|
||||
Meta[String].imap(Language.unsafe)(_.iso3)
|
||||
|
||||
implicit val sslType: Meta[SSLType] =
|
||||
Meta[String].imap(DoobieMeta.readSSLType)(DoobieMeta.sslTypeString)
|
||||
Meta[String].imap(EmilUtil.unsafeReadSSLType)(EmilUtil.sslTypeString)
|
||||
|
||||
implicit val mailAddress: Meta[MailAddress] =
|
||||
Meta[String].imap(str => MailAddress.parse(str).fold(sys.error, identity))(_.asUnicodeString)
|
||||
Meta[String].imap(EmilUtil.unsafeReadMailAddress)(EmilUtil.mailAddressString)
|
||||
}
|
||||
|
||||
object DoobieMeta extends DoobieMeta {
|
||||
import org.log4s._
|
||||
private val logger = getLogger
|
||||
|
||||
private def readSSLType(str: String): SSLType =
|
||||
str.toLowerCase match {
|
||||
case "ssl" => SSLType.SSL
|
||||
case "starttls" => SSLType.StartTLS
|
||||
case "none" => SSLType.NoEncryption
|
||||
case _ => sys.error(s"Invalid ssl-type: $str")
|
||||
}
|
||||
|
||||
private def sslTypeString(st: SSLType): String =
|
||||
st match {
|
||||
case SSLType.SSL => "ssl"
|
||||
case SSLType.StartTLS => "starttls"
|
||||
case SSLType.NoEncryption => "none"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ 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._
|
||||
@ -10,11 +13,11 @@ import emil.{MailAddress, SSLType}
|
||||
case class RUserEmail(
|
||||
id: Ident,
|
||||
uid: Ident,
|
||||
name: String,
|
||||
name: Ident,
|
||||
smtpHost: String,
|
||||
smtpPort: Int,
|
||||
smtpUser: String,
|
||||
smtpPassword: Password,
|
||||
smtpPort: Option[Int],
|
||||
smtpUser: Option[String],
|
||||
smtpPassword: Option[Password],
|
||||
smtpSsl: SSLType,
|
||||
smtpCertCheck: Boolean,
|
||||
mailFrom: MailAddress,
|
||||
@ -24,6 +27,67 @@ case class RUserEmail(
|
||||
|
||||
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 apply(
|
||||
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 {
|
||||
@ -65,7 +129,54 @@ object RUserEmail {
|
||||
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, from, and(cond)).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 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)
|
||||
}
|
||||
|
@ -2,19 +2,36 @@ module Comp.EmailSettingsForm exposing
|
||||
( Model
|
||||
, Msg
|
||||
, emptyModel
|
||||
, getSettings
|
||||
, init
|
||||
, 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 (onInput)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +39,22 @@ emptyModel : Model
|
||||
emptyModel =
|
||||
{ settings = Api.Model.EmailSettings.empty
|
||||
, name = ""
|
||||
, host = ""
|
||||
, portField = Comp.IntField.init (Just 0) Nothing "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
|
||||
}
|
||||
|
||||
|
||||
@ -29,21 +62,103 @@ init : EmailSettings -> Model
|
||||
init ems =
|
||||
{ settings = ems
|
||||
, name = ems.name
|
||||
, host = ems.smtpHost
|
||||
, portField = Comp.IntField.init (Just 0) Nothing "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
|
||||
}
|
||||
, 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 =
|
||||
True
|
||||
model.host /= "" && model.name /= ""
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
( model, Cmd.none )
|
||||
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
|
||||
@ -61,8 +176,81 @@ view model =
|
||||
[ type_ "text"
|
||||
, value model.name
|
||||
, onInput SetName
|
||||
, placeholder "Connection name"
|
||||
, 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 "fifteen 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 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)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
@ -17,6 +17,7 @@ import Comp.YesNoDimmer
|
||||
import Data.Flags exposing (Flags)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick, onInput)
|
||||
|
||||
|
||||
type alias Model =
|
||||
@ -25,6 +26,7 @@ type alias Model =
|
||||
, viewMode : ViewMode
|
||||
, formError : Maybe String
|
||||
, loading : Bool
|
||||
, query : String
|
||||
, deleteConfirm : Comp.YesNoDimmer.Model
|
||||
}
|
||||
|
||||
@ -36,6 +38,7 @@ emptyModel =
|
||||
, viewMode = Table
|
||||
, formError = Nothing
|
||||
, loading = False
|
||||
, query = ""
|
||||
, deleteConfirm = Comp.YesNoDimmer.emptyModel
|
||||
}
|
||||
|
||||
@ -53,18 +56,139 @@ type ViewMode
|
||||
type Msg
|
||||
= TableMsg Comp.EmailSettingsTable.Msg
|
||||
| FormMsg Comp.EmailSettingsForm.Msg
|
||||
| SetQuery String
|
||||
| InitNew
|
||||
| YesNoMsg Comp.YesNoDimmer.Msg
|
||||
| RequestDelete
|
||||
| SetViewMode ViewMode
|
||||
|
||||
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update flags msg model =
|
||||
( model, Cmd.none )
|
||||
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
|
||||
in
|
||||
( { model | tableModel = tm }, 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 ->
|
||||
( { model | query = str }, Cmd.none )
|
||||
|
||||
YesNoMsg m ->
|
||||
let
|
||||
( dm, flag ) =
|
||||
Comp.YesNoDimmer.update m model.deleteConfirm
|
||||
in
|
||||
( { model | deleteConfirm = dm }, Cmd.none )
|
||||
|
||||
RequestDelete ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SetViewMode m ->
|
||||
( { model | viewMode = m }, Cmd.none )
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
case model.viewMode of
|
||||
Table ->
|
||||
Html.map TableMsg (Comp.EmailSettingsTable.view model.tableModel)
|
||||
viewTable model
|
||||
|
||||
Form ->
|
||||
Html.map FormMsg (Comp.EmailSettingsForm.view model.formModel)
|
||||
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" ]
|
||||
[ 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" ] []
|
||||
]
|
||||
]
|
||||
|
108
modules/webapp/src/main/elm/Comp/IntField.elm
Normal file
108
modules/webapp/src/main/elm/Comp/IntField.elm
Normal file
@ -0,0 +1,108 @@
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= SetValue String
|
||||
|
||||
|
||||
init : Maybe Int -> Maybe Int -> String -> Model
|
||||
init min max label =
|
||||
{ min = min
|
||||
, max = max
|
||||
, label = label
|
||||
, error = Nothing
|
||||
, lastInput = ""
|
||||
}
|
||||
|
||||
|
||||
tooLow : Model -> Int -> Bool
|
||||
tooLow model n =
|
||||
Maybe.map ((<) n) model.min
|
||||
|> Maybe.withDefault False
|
||||
|
||||
|
||||
tooHigh : Model -> Int -> Bool
|
||||
tooHigh model n =
|
||||
Maybe.map ((>) n) model.max
|
||||
|> Maybe.withDefault False
|
||||
|
||||
|
||||
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 ->
|
||||
( { m | error = Just ("'" ++ str ++ "' is not a valid number!") }
|
||||
, Nothing
|
||||
)
|
||||
|
||||
|
||||
view : Maybe Int -> Model -> Html Msg
|
||||
view nval model =
|
||||
div
|
||||
[ classList
|
||||
[ ( "field", 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
|
||||
]
|
||||
]
|
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
|
||||
]
|
||||
[]
|
||||
]
|
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"
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user