Starting with mail settings

This commit is contained in:
Eike Kettner 2020-01-05 00:12:23 +01:00
parent 9020d9aa3b
commit 2e3454c7a1
12 changed files with 519 additions and 4 deletions

View File

@ -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")).

View File

@ -1212,8 +1212,172 @@ 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`.
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.
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.
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`.
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`.
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 with all attachments of the item
with `id` as mail attachments. If the item has no attachments,
then the mail is sent without any. If the item's attachments
exceed a specific size, the mail will not be sent.
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"
components:
schemas:
SimpleMail:
description: |
A simple e-mail.
required:
- recipients
- subject
- body
properties:
recipients:
type: array
items:
type: string
subject:
type: string
body:
type: string
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
- smtpPort
- smtpUser
- smtpPassword
- from
- sslType
- certificateCheck
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
sslType:
type: string
certificateCheck:
type: boolean
CheckFileResult:
description: |
Results when searching for file checksums.
@ -2157,7 +2321,7 @@ components:
id:
name: id
in: path
description: A identifier
description: An identifier
required: true
schema:
type: string
@ -2182,3 +2346,10 @@ components:
required: false
schema:
type: string
name:
name: name
in: path
description: An e-mail connection name
required: true
schema:
type: string

View File

@ -0,0 +1,16 @@
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 not null,
"smtp_user" varchar(254) not null,
"smtp_password" varchar(254) not null,
"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")
);

View File

@ -12,6 +12,8 @@ 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._
trait DoobieMeta {
@ -88,9 +90,31 @@ trait DoobieMeta {
implicit val metaLanguage: Meta[Language] =
Meta[String].imap(Language.unsafe)(_.iso3)
implicit val sslType: Meta[SSLType] =
Meta[String].imap(DoobieMeta.readSSLType)(DoobieMeta.sslTypeString)
implicit val mailAddress: Meta[MailAddress] =
Meta[String].imap(str => MailAddress.parse(str).fold(sys.error, identity))(_.asUnicodeString)
}
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"
}
}

View File

@ -0,0 +1,71 @@
package docspell.store.records
import doobie._
import doobie.implicits._
import docspell.common._
import docspell.store.impl.Column
import docspell.store.impl.Implicits._
import emil.{MailAddress, SSLType}
case class RUserEmail(
id: Ident,
uid: Ident,
name: String,
smtpHost: String,
smtpPort: Int,
smtpUser: String,
smtpPassword: Password,
smtpSsl: SSLType,
smtpCertCheck: Boolean,
mailFrom: MailAddress,
mailReplyTo: Option[MailAddress],
created: Timestamp
) {}
object RUserEmail {
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 findByUser(userId: Ident): ConnectionIO[Vector[RUserEmail]] =
selectSimple(all, table, uid.is(userId)).query[RUserEmail].to[Vector]
}

View File

@ -0,0 +1,68 @@
module Comp.EmailSettingsForm exposing
( Model
, Msg
, emptyModel
, update
, view
)
import Api.Model.EmailSettings exposing (EmailSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
type alias Model =
{ settings : EmailSettings
, name : String
}
emptyModel : Model
emptyModel =
{ settings = Api.Model.EmailSettings.empty
, name = ""
}
init : EmailSettings -> Model
init ems =
{ settings = ems
, name = ems.name
}
type Msg
= SetName String
isValid : Model -> Bool
isValid model =
True
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
( model, 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"
]
[]
]
]

View File

@ -0,0 +1,70 @@
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 (..)
type alias Model =
{ tableModel : Comp.EmailSettingsTable.Model
, formModel : Comp.EmailSettingsForm.Model
, viewMode : ViewMode
, formError : Maybe String
, loading : Bool
, deleteConfirm : Comp.YesNoDimmer.Model
}
emptyModel : Model
emptyModel =
{ tableModel = Comp.EmailSettingsTable.emptyModel
, formModel = Comp.EmailSettingsForm.emptyModel
, viewMode = Table
, formError = Nothing
, loading = False
, deleteConfirm = Comp.YesNoDimmer.emptyModel
}
init : ( Model, Cmd Msg )
init =
( emptyModel, Cmd.none )
type ViewMode
= Table
| Form
type Msg
= TableMsg Comp.EmailSettingsTable.Msg
| FormMsg Comp.EmailSettingsForm.Msg
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags msg model =
( model, Cmd.none )
view : Model -> Html Msg
view model =
case model.viewMode of
Table ->
Html.map TableMsg (Comp.EmailSettingsTable.view model.tableModel)
Form ->
Html.map FormMsg (Comp.EmailSettingsForm.view model.formModel)

View File

@ -0,0 +1,49 @@
module Comp.EmailSettingsTable exposing
( Model
, Msg
, emptyModel
, update
, view
)
import Api.Model.EmailSettings exposing (EmailSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
type alias Model =
{ emailSettings : List EmailSettings
}
emptyModel : Model
emptyModel =
init []
init : List EmailSettings -> Model
init ems =
{ emailSettings = ems
}
type Msg
= Select EmailSettings
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
( model, Cmd.none )
view : Model -> Html Msg
view model =
table [ class "ui table" ]
[ thead []
[ th [] [ text "Name" ]
, th [] [ text "Host/Port" ]
, th [] [ text "From" ]
]
, tbody []
[]
]

View File

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

View File

@ -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 (..)
@ -21,3 +22,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 )

View File

@ -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" ]

View File

@ -9,6 +9,7 @@ object Dependencies {
val BitpeaceVersion = "0.4.2"
val CirceVersion = "0.12.3"
val DoobieVersion = "0.8.8"
val EmilVersion = "0.1.1"
val FastparseVersion = "2.1.3"
val FlywayVersion = "6.1.3"
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"),