From 2e3454c7a199569714e5de889a96301ed51fded3 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Sun, 5 Jan 2020 00:12:23 +0100 Subject: [PATCH] Starting with mail settings --- build.sbt | 6 +- .../src/main/resources/docspell-openapi.yml | 173 +++++++++++++++++- .../postgresql/V1.1.0__useremail.sql | 16 ++ .../docspell/store/impl/DoobieMeta.scala | 24 +++ .../docspell/store/records/RUserEmail.scala | 71 +++++++ .../src/main/elm/Comp/EmailSettingsForm.elm | 68 +++++++ .../src/main/elm/Comp/EmailSettingsManage.elm | 70 +++++++ .../src/main/elm/Comp/EmailSettingsTable.elm | 49 +++++ .../src/main/elm/Page/UserSettings/Data.elm | 5 + .../src/main/elm/Page/UserSettings/Update.elm | 8 + .../src/main/elm/Page/UserSettings/View.elm | 27 ++- project/Dependencies.scala | 6 + 12 files changed, 519 insertions(+), 4 deletions(-) create mode 100644 modules/store/src/main/resources/db/migration/postgresql/V1.1.0__useremail.sql create mode 100644 modules/store/src/main/scala/docspell/store/records/RUserEmail.scala create mode 100644 modules/webapp/src/main/elm/Comp/EmailSettingsForm.elm create mode 100644 modules/webapp/src/main/elm/Comp/EmailSettingsManage.elm create mode 100644 modules/webapp/src/main/elm/Comp/EmailSettingsTable.elm diff --git a/build.sbt b/build.sbt index 7284171c..ad809ec5 100644 --- a/build.sbt +++ b/build.sbt @@ -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")). diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 88eba465..9f8383f5 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -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 diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.1.0__useremail.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.1.0__useremail.sql new file mode 100644 index 00000000..3595f5a9 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/postgresql/V1.1.0__useremail.sql @@ -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") +); diff --git a/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala b/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala index b731406e..98d3b594 100644 --- a/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala +++ b/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala @@ -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" + } + } diff --git a/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala b/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala new file mode 100644 index 00000000..5f564b4f --- /dev/null +++ b/modules/store/src/main/scala/docspell/store/records/RUserEmail.scala @@ -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] + +} diff --git a/modules/webapp/src/main/elm/Comp/EmailSettingsForm.elm b/modules/webapp/src/main/elm/Comp/EmailSettingsForm.elm new file mode 100644 index 00000000..c3785eca --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/EmailSettingsForm.elm @@ -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" + ] + [] + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/EmailSettingsManage.elm b/modules/webapp/src/main/elm/Comp/EmailSettingsManage.elm new file mode 100644 index 00000000..293d2df8 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/EmailSettingsManage.elm @@ -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) diff --git a/modules/webapp/src/main/elm/Comp/EmailSettingsTable.elm b/modules/webapp/src/main/elm/Comp/EmailSettingsTable.elm new file mode 100644 index 00000000..d09c9bee --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/EmailSettingsTable.elm @@ -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 [] + [] + ] diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Data.elm b/modules/webapp/src/main/elm/Page/UserSettings/Data.elm index e56608a7..905c6125 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/Data.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/Data.elm @@ -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 diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm index 40e34e18..0a17d578 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm @@ -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 ) diff --git a/modules/webapp/src/main/elm/Page/UserSettings/View.elm b/modules/webapp/src/main/elm/Page/UserSettings/View.elm index 6e1aee04..d89d1daa 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/View.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/View.elm @@ -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" ] diff --git a/project/Dependencies.scala b/project/Dependencies.scala index f5d467ec..33dcd904 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -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"),