Finish mail settings

This commit is contained in:
Eike Kettner 2020-01-07 00:20:28 +01:00
parent f235f3a030
commit 32050a9faf
11 changed files with 391 additions and 57 deletions

View File

@ -3,6 +3,8 @@ package docspell.backend.ops
import cats.effect._
import cats.implicits._
import cats.data.OptionT
import emil.{MailAddress, SSLType}
import docspell.common._
import docspell.store._
import docspell.store.records.RUserEmail
@ -11,33 +13,71 @@ trait OMail[F[_]] {
def getSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]]
def createSettings(data: F[RUserEmail]): F[AddResult]
def findSettings(accId: AccountId, name: Ident): OptionT[F, RUserEmail]
def updateSettings(accId: AccountId, name: Ident, data: RUserEmail): F[Int]
def createSettings(accId: AccountId, data: OMail.SmtpSettings): F[AddResult]
def updateSettings(accId: AccountId, name: Ident, data: OMail.SmtpSettings): F[Int]
def deleteSettings(accId: AccountId, name: Ident): F[Int]
}
object OMail {
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]): 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 findSettings(accId: AccountId, name: Ident): OptionT[F, RUserEmail] =
OptionT(store.transact(RUserEmail.getByName(accId, name)))
def updateSettings(accId: AccountId, name: Ident, data: RUserEmail): F[Int] = {
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))
n <- OptionT.liftF(RUserEmail.update(um.id, data))
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))
})
}

View File

@ -2,17 +2,21 @@ 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 {
@ -29,10 +33,51 @@ object MailSettingsRoutes {
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 {
in <- req.as[EmailSettings]
resp <- Ok(BasicResult(false, "not implemented"))
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
}
@ -50,4 +95,29 @@ object MailSettingsRoutes {
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
)
}
}

View File

@ -3,9 +3,9 @@ CREATE TABLE "useremail" (
"uid" varchar(254) not null,
"name" varchar(254) not null,
"smtp_host" varchar(254) not null,
"smtp_port" int not null,
"smtp_user" varchar(254) not null,
"smtp_password" 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,

View File

@ -57,7 +57,7 @@ object RUserEmail {
now
)
def apply(
def fromAccount(
accId: AccountId,
name: Ident,
smtpHost: String,
@ -130,17 +130,21 @@ object RUserEmail {
).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
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]
@ -162,7 +166,7 @@ object RUserEmail {
case None => Seq.empty
})
selectSimple(all, from, and(cond)).query[RUserEmail]
(selectSimple(all.map(_.prefix("m")), from, and(cond)) ++ orderBy(mName.f)).query[RUserEmail]
}
def findByAccount(
@ -174,6 +178,20 @@ object RUserEmail {
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)

View File

@ -1,8 +1,10 @@
module Api exposing
( cancelJob
, changePassword
, createMailSettings
, deleteEquip
, deleteItem
, deleteMailSettings
, deleteOrg
, deletePerson
, deleteSource
@ -15,6 +17,7 @@ module Api exposing
, getItemProposals
, getJobQueueState
, getJobQueueStateIn
, getMailSettings
, getOrgLight
, getOrganizations
, getPersons
@ -60,6 +63,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)
@ -99,6 +104,57 @@ import Util.File
import Util.Http as Http2
--- 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

View File

@ -4,6 +4,7 @@ module Comp.EmailSettingsForm exposing
, emptyModel
, getSettings
, init
, isValid
, update
, view
)
@ -40,7 +41,7 @@ emptyModel =
{ settings = Api.Model.EmailSettings.empty
, name = ""
, host = ""
, portField = Comp.IntField.init (Just 0) Nothing "SMTP Port"
, portField = Comp.IntField.init (Just 0) Nothing True "SMTP Port"
, portNum = Nothing
, user = Nothing
, passField = Comp.PasswordInput.init
@ -63,7 +64,7 @@ init ems =
{ settings = ems
, name = ems.name
, host = ems.smtpHost
, portField = Comp.IntField.init (Just 0) Nothing "SMTP Port"
, portField = Comp.IntField.init (Just 0) Nothing True "SMTP Port"
, portNum = ems.smtpPort
, user = ems.smtpUser
, passField = Comp.PasswordInput.init
@ -184,7 +185,7 @@ view model =
]
]
, div [ class "fields" ]
[ div [ class "fifteen wide required field" ]
[ div [ class "thirteen wide required field" ]
[ label [] [ text "SMTP Host" ]
, input
[ type_ "text"
@ -194,7 +195,11 @@ view model =
]
[]
]
, Html.map PortMsg (Comp.IntField.view model.portNum model.portField)
, Html.map PortMsg
(Comp.IntField.view model.portNum
"three wide field"
model.portField
)
]
, div [ class "two fields" ]
[ div [ class "field" ]

View File

@ -18,6 +18,8 @@ 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 =
@ -43,9 +45,9 @@ emptyModel =
}
init : ( Model, Cmd Msg )
init =
( emptyModel, Cmd.none )
init : Flags -> ( Model, Cmd Msg )
init flags =
( emptyModel, Api.getMailSettings flags "" MailSettingsResp )
type ViewMode
@ -61,6 +63,10 @@ type Msg
| 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 )
@ -84,8 +90,27 @@ update flags msg model =
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
( { model | tableModel = tm }, Cmd.map TableMsg tc )
( m2, Cmd.map TableMsg tc )
FormMsg m ->
let
@ -95,21 +120,84 @@ update flags msg model =
( { model | formModel = fm }, Cmd.map FormMsg fc )
SetQuery str ->
( { model | query = str }, Cmd.none )
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.none )
( { model | deleteConfirm = dm }, cmd )
RequestDelete ->
( model, Cmd.none )
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 =
@ -171,10 +259,18 @@ viewForm model =
[ Maybe.withDefault "" model.formError |> text
]
, div [ class "ui divider" ] []
, button [ class "ui primary button" ]
, button
[ class "ui primary button"
, onClick Submit
, href "#"
]
[ text "Submit"
]
, a [ class "ui secondary button", onClick (SetViewMode Table), href "" ]
, a
[ class "ui secondary button"
, onClick (SetViewMode Table)
, href ""
]
[ text "Cancel"
]
, if model.formModel.settings.name /= "" then

View File

@ -2,6 +2,7 @@ module Comp.EmailSettingsTable exposing
( Model
, Msg
, emptyModel
, init
, update
, view
)
@ -9,10 +10,12 @@ module Comp.EmailSettingsTable exposing
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
}
@ -24,6 +27,7 @@ emptyModel =
init : List EmailSettings -> Model
init ems =
{ emailSettings = ems
, selected = Nothing
}
@ -33,17 +37,40 @@ type Msg
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
( model, Cmd.none )
case msg of
Select ems ->
( { model | selected = Just ems }, Cmd.none )
view : Model -> Html Msg
view model =
table [ class "ui table" ]
table [ class "ui selectable pointer table" ]
[ thead []
[ th [] [ text "Name" ]
[ 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 ]
]

View File

@ -11,6 +11,7 @@ type alias Model =
, label : String
, error : Maybe String
, lastInput : String
, optional : Bool
}
@ -18,26 +19,27 @@ type Msg
= SetValue String
init : Maybe Int -> Maybe Int -> String -> Model
init min max label =
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 False
|> Maybe.withDefault (not model.optional)
tooHigh : Model -> Int -> Bool
tooHigh model n =
Maybe.map ((>) n) model.max
|> Maybe.withDefault False
|> Maybe.withDefault (not model.optional)
update : Msg -> Model -> ( Model, Maybe Int )
@ -75,16 +77,20 @@ update msg model =
( { m | error = Nothing }, Just n )
Nothing ->
( { m | error = Just ("'" ++ str ++ "' is not a valid number!") }
, 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 -> Model -> Html Msg
view nval model =
view : Maybe Int -> String -> Model -> Html Msg
view nval classes model =
div
[ classList
[ ( "field", True )
[ ( classes, True )
, ( "error", model.error /= Nothing )
]
]

View File

@ -13,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

View File

@ -88,6 +88,10 @@
background-color: #d8dfe5;
}
.ui.selectable.pointer.table tr {
cursor: pointer;
}
span.small-info {
font-size: smaller;
color: rgba(0,0,0,0.6);