From c9de74fd912bd5504eda94843e9fa529115b5610 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Tue, 5 May 2020 22:48:08 +0200 Subject: [PATCH] Add imap settings --- .../scala/docspell/backend/ops/OMail.scala | 97 +++++- .../src/main/resources/docspell-openapi.yml | 134 +++++++- .../routes/MailSettingsRoutes.scala | 112 ++++++- .../routes/NotifyDueItemsRoutes.scala | 2 +- .../db/migration/mariadb/V1.5.0__userimap.sql | 14 + .../migration/postgresql/V1.5.0__userimap.sql | 14 + .../docspell/store/records/RUserImap.scala | 201 ++++++++++++ modules/webapp/src/main/elm/Api.elm | 56 +++- .../src/main/elm/Comp/ImapSettingsForm.elm | 226 ++++++++++++++ .../src/main/elm/Comp/ImapSettingsManage.elm | 288 ++++++++++++++++++ .../src/main/elm/Comp/ImapSettingsTable.elm | 74 +++++ .../src/main/elm/Page/UserSettings/Data.elm | 5 + .../src/main/elm/Page/UserSettings/Update.elm | 15 + .../src/main/elm/Page/UserSettings/View.elm | 21 +- 14 files changed, 1221 insertions(+), 38 deletions(-) create mode 100644 modules/store/src/main/resources/db/migration/mariadb/V1.5.0__userimap.sql create mode 100644 modules/store/src/main/resources/db/migration/postgresql/V1.5.0__userimap.sql create mode 100644 modules/store/src/main/scala/docspell/store/records/RUserImap.scala create mode 100644 modules/webapp/src/main/elm/Comp/ImapSettingsForm.elm create mode 100644 modules/webapp/src/main/elm/Comp/ImapSettingsManage.elm create mode 100644 modules/webapp/src/main/elm/Comp/ImapSettingsTable.elm diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala index c334964e..4c4ae045 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala @@ -12,19 +12,29 @@ import docspell.common._ import docspell.store._ import docspell.store.records._ import docspell.store.queries.QMails -import OMail.{ItemMail, Sent, SmtpSettings} +import OMail.{ImapSettings, ItemMail, Sent, SmtpSettings} trait OMail[F[_]] { - def getSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]] + def getSmtpSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]] - def findSettings(accId: AccountId, name: Ident): OptionT[F, RUserEmail] + def findSmtpSettings(accId: AccountId, name: Ident): OptionT[F, RUserEmail] - def createSettings(accId: AccountId, data: SmtpSettings): F[AddResult] + def createSmtpSettings(accId: AccountId, data: SmtpSettings): F[AddResult] - def updateSettings(accId: AccountId, name: Ident, data: OMail.SmtpSettings): F[Int] + def updateSmtpSettings(accId: AccountId, name: Ident, data: OMail.SmtpSettings): F[Int] - def deleteSettings(accId: AccountId, name: Ident): F[Int] + def deleteSmtpSettings(accId: AccountId, name: Ident): F[Int] + + def getImapSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserImap]] + + def findImapSettings(accId: AccountId, name: Ident): OptionT[F, RUserImap] + + def createImapSettings(accId: AccountId, data: ImapSettings): F[AddResult] + + def updateImapSettings(accId: AccountId, name: Ident, data: OMail.ImapSettings): F[Int] + + def deleteImapSettings(accId: AccountId, name: Ident): F[Int] def sendMail(accId: AccountId, name: Ident, m: ItemMail): F[SendResult] @@ -103,15 +113,41 @@ object OMail { ) } + case class ImapSettings( + name: Ident, + imapHost: String, + imapPort: Option[Int], + imapUser: Option[String], + imapPassword: Option[Password], + imapSsl: SSLType, + imapCertCheck: Boolean + ) { + + def toRecord(accId: AccountId) = + RUserImap.fromAccount( + accId, + name, + imapHost, + imapPort, + imapUser, + imapPassword, + imapSsl, + imapCertCheck + ) + } + def apply[F[_]: Effect](store: Store[F], emil: Emil[F]): Resource[F, OMail[F]] = Resource.pure[F, OMail[F]](new OMail[F] { - def getSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserEmail]] = + def getSmtpSettings( + accId: AccountId, + nameQ: Option[String] + ): F[Vector[RUserEmail]] = store.transact(RUserEmail.findByAccount(accId, nameQ)) - def findSettings(accId: AccountId, name: Ident): OptionT[F, RUserEmail] = + def findSmtpSettings(accId: AccountId, name: Ident): OptionT[F, RUserEmail] = OptionT(store.transact(RUserEmail.getByName(accId, name))) - def createSettings(accId: AccountId, s: SmtpSettings): F[AddResult] = + def createSmtpSettings(accId: AccountId, s: SmtpSettings): F[AddResult] = (for { ru <- OptionT(store.transact(s.toRecord(accId).value)) ins = RUserEmail.insert(ru) @@ -119,7 +155,11 @@ object OMail { 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] = { + def updateSmtpSettings( + accId: AccountId, + name: Ident, + data: SmtpSettings + ): F[Int] = { val op = for { um <- OptionT(RUserEmail.getByName(accId, name)) ru <- data.toRecord(accId) @@ -129,12 +169,43 @@ object OMail { store.transact(op.value).map(_.getOrElse(0)) } - def deleteSettings(accId: AccountId, name: Ident): F[Int] = + def deleteSmtpSettings(accId: AccountId, name: Ident): F[Int] = store.transact(RUserEmail.delete(accId, name)) + def getImapSettings(accId: AccountId, nameQ: Option[String]): F[Vector[RUserImap]] = + store.transact(RUserImap.findByAccount(accId, nameQ)) + + def findImapSettings(accId: AccountId, name: Ident): OptionT[F, RUserImap] = + OptionT(store.transact(RUserImap.getByName(accId, name))) + + def createImapSettings(accId: AccountId, data: ImapSettings): F[AddResult] = + (for { + ru <- OptionT(store.transact(data.toRecord(accId).value)) + ins = RUserImap.insert(ru) + exists = RUserImap.exists(ru.uid, ru.name) + res <- OptionT.liftF(store.add(ins, exists)) + } yield res).getOrElse(AddResult.Failure(new Exception("User not found"))) + + def updateImapSettings( + accId: AccountId, + name: Ident, + data: OMail.ImapSettings + ): F[Int] = { + val op = for { + um <- OptionT(RUserImap.getByName(accId, name)) + ru <- data.toRecord(accId) + n <- OptionT.liftF(RUserImap.update(um.id, ru)) + } yield n + + store.transact(op.value).map(_.getOrElse(0)) + } + + def deleteImapSettings(accId: AccountId, name: Ident): F[Int] = + store.transact(RUserImap.delete(accId, name)) + def sendMail(accId: AccountId, name: Ident, m: ItemMail): F[SendResult] = { - val getSettings: OptionT[F, RUserEmail] = + val getSmtpSettings: OptionT[F, RUserEmail] = OptionT(store.transact(RUserEmail.getByName(accId, name))) def createMail(sett: RUserEmail): OptionT[F, Mail[F]] = { @@ -198,7 +269,7 @@ object OMail { } (for { - mailCfg <- getSettings + mailCfg <- getSmtpSettings mail <- createMail(mailCfg) mid <- OptionT.liftF(sendMail(mailCfg.toMailConfig, mail)) res <- mid.traverse(id => OptionT.liftF(storeMail(id, mailCfg))) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 61ad220a..642a8bba 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1414,7 +1414,7 @@ paths: application/json: schema: $ref: "#/components/schemas/BasicResult" - /sec/email/settings: + /sec/email/settings/smtp: get: tags: [ E-Mail ] summary: List email settings for current user. @@ -1456,7 +1456,7 @@ paths: application/json: schema: $ref: "#/components/schemas/BasicResult" - /sec/email/settings/{name}: + /sec/email/settings/smtp/{name}: parameters: - $ref: "#/components/parameters/name" get: @@ -1507,6 +1507,99 @@ paths: application/json: schema: $ref: "#/components/schemas/BasicResult" + /sec/email/settings/imap: + 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 imap connections that can be used to + retrieve 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/ImapSettingsList" + 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/ImapSettings" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + /sec/email/settings/imap/{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/ImapSettings" + 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/ImapSettings" + 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: @@ -1664,6 +1757,43 @@ paths: components: schemas: + ImapSettingsList: + description: | + A list of user email settings. + required: + - items + properties: + items: + type: array + items: + $ref: "#/components/schemas/ImapSettings" + ImapSettings: + description: | + IMAP settings for sending mail. + required: + - name + - imapHost + - from + - sslType + - ignoreCertificates + properties: + name: + type: string + format: ident + imapHost: + type: string + imapPort: + type: integer + format: int32 + imapUser: + type: string + imapPassword: + type: string + format: password + sslType: + type: string + ignoreCertificates: + type: boolean CalEventCheckResult: description: | The result of checking a calendar event string. diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala index 38a321a8..a3bf338f 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala @@ -15,7 +15,7 @@ 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.records.{RUserEmail, RUserImap} import docspell.restserver.conv.Conversions import docspell.restserver.http4s.QueryParam @@ -26,25 +26,38 @@ object MailSettingsRoutes { import dsl._ HttpRoutes.of { - case GET -> Root :? QueryParam.QueryOpt(q) => + case GET -> Root / "smtp" :? QueryParam.QueryOpt(q) => for { - list <- backend.mail.getSettings(user.account, q.map(_.q)) + list <- backend.mail.getSmtpSettings(user.account, q.map(_.q)) res = list.map(convert) resp <- Ok(EmailSettingsList(res.toList)) } yield resp - case GET -> Root / Ident(name) => + case GET -> Root / "imap" :? QueryParam.QueryOpt(q) => + for { + list <- backend.mail.getImapSettings(user.account, q.map(_.q)) + res = list.map(convert) + resp <- Ok(ImapSettingsList(res.toList)) + } yield resp + + case GET -> Root / "smtp" / Ident(name) => (for { - ems <- backend.mail.findSettings(user.account, name) + ems <- backend.mail.findSmtpSettings(user.account, name) resp <- OptionT.liftF(Ok(convert(ems))) } yield resp).getOrElseF(NotFound()) - case req @ POST -> Root => + case GET -> Root / "imap" / Ident(name) => + (for { + ems <- backend.mail.findImapSettings(user.account, name) + resp <- OptionT.liftF(Ok(convert(ems))) + } yield resp).getOrElseF(NotFound()) + + case req @ POST -> Root / "smtp" => (for { in <- OptionT.liftF(req.as[EmailSettings]) - ru = makeSettings(in) + ru = makeSmtpSettings(in) up <- OptionT.liftF( - ru.traverse(r => backend.mail.createSettings(user.account, r)) + ru.traverse(r => backend.mail.createSmtpSettings(user.account, r)) ) resp <- OptionT.liftF( Ok( @@ -56,12 +69,29 @@ object MailSettingsRoutes { ) } yield resp).getOrElseF(NotFound()) - case req @ PUT -> Root / Ident(name) => + case req @ POST -> Root / "imap" => + (for { + in <- OptionT.liftF(req.as[ImapSettings]) + ru = makeImapSettings(in) + up <- OptionT.liftF( + ru.traverse(r => backend.mail.createImapSettings(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 / "smtp" / Ident(name) => (for { in <- OptionT.liftF(req.as[EmailSettings]) - ru = makeSettings(in) + ru = makeSmtpSettings(in) up <- OptionT.liftF( - ru.traverse(r => backend.mail.updateSettings(user.account, name, r)) + ru.traverse(r => backend.mail.updateSmtpSettings(user.account, name, r)) ) resp <- OptionT.liftF( Ok( @@ -75,16 +105,43 @@ object MailSettingsRoutes { ) } yield resp).getOrElseF(NotFound()) - case DELETE -> Root / Ident(name) => + case req @ PUT -> Root / "imap" / Ident(name) => + (for { + in <- OptionT.liftF(req.as[ImapSettings]) + ru = makeImapSettings(in) + up <- OptionT.liftF( + ru.traverse(r => backend.mail.updateImapSettings(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 / "smtp" / Ident(name) => for { - n <- backend.mail.deleteSettings(user.account, name) + n <- backend.mail.deleteSmtpSettings(user.account, name) + resp <- Ok( + if (n > 0) BasicResult(true, "Mail settings removed") + else BasicResult(false, "Mail settings could not be removed") + ) + } yield resp + + case DELETE -> Root / "imap" / Ident(name) => + for { + n <- backend.mail.deleteImapSettings(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 = @@ -100,7 +157,18 @@ object MailSettingsRoutes { !ru.smtpCertCheck ) - def makeSettings(ems: EmailSettings): Either[String, OMail.SmtpSettings] = { + def convert(ru: RUserImap): ImapSettings = + ImapSettings( + ru.name, + ru.imapHost, + ru.imapPort, + ru.imapUser, + ru.imapPassword, + ru.imapSsl.name, + !ru.imapCertCheck + ) + + def makeSmtpSettings(ems: EmailSettings): Either[String, OMail.SmtpSettings] = { def readMail(str: String): Either[String, MailAddress] = MailAddress.parse(str).left.map(err => s"E-Mail address '$str' invalid: $err") @@ -122,6 +190,18 @@ object MailSettingsRoutes { from, repl ) - } + + def makeImapSettings(ims: ImapSettings): Either[String, OMail.ImapSettings] = + for { + sslt <- SSLType.fromString(ims.sslType) + } yield OMail.ImapSettings( + ims.name, + ims.imapHost, + ims.imapPort, + ims.imapUser, + ims.imapPassword, + sslt, + !ims.ignoreCertificates + ) } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala index 069346d4..88c7a67f 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala @@ -91,7 +91,7 @@ object NotifyDueItemsRoutes { texc <- backend.tag.loadAll(task.args.tagsExclude) conn <- backend.mail - .getSettings(account, None) + .getSmtpSettings(account, None) .map( _.find(_.name == task.args.smtpConnection) .map(_.name) diff --git a/modules/store/src/main/resources/db/migration/mariadb/V1.5.0__userimap.sql b/modules/store/src/main/resources/db/migration/mariadb/V1.5.0__userimap.sql new file mode 100644 index 00000000..2795b487 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/mariadb/V1.5.0__userimap.sql @@ -0,0 +1,14 @@ +CREATE TABLE `userimap` ( + `id` varchar(254) not null primary key, + `uid` varchar(254) not null, + `name` varchar(254) not null, + `imap_host` varchar(254) not null, + `imap_port` int, + `imap_user` varchar(254), + `imap_password` varchar(254), + `imap_ssl` varchar(254) not null, + `imap_certcheck` boolean not null, + `created` timestamp not null, + unique (`uid`, `name`), + foreign key (`uid`) references `user_`(`uid`) +); diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.5.0__userimap.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.5.0__userimap.sql new file mode 100644 index 00000000..7b60396f --- /dev/null +++ b/modules/store/src/main/resources/db/migration/postgresql/V1.5.0__userimap.sql @@ -0,0 +1,14 @@ +CREATE TABLE "userimap" ( + "id" varchar(254) not null primary key, + "uid" varchar(254) not null, + "name" varchar(254) not null, + "imap_host" varchar(254) not null, + "imap_port" int, + "imap_user" varchar(254), + "imap_password" varchar(254), + "imap_ssl" varchar(254) not null, + "imap_certcheck" boolean not null, + "created" timestamp not null, + unique ("uid", "name"), + foreign key ("uid") references "user_"("uid") +); diff --git a/modules/store/src/main/scala/docspell/store/records/RUserImap.scala b/modules/store/src/main/scala/docspell/store/records/RUserImap.scala new file mode 100644 index 00000000..433fdd1a --- /dev/null +++ b/modules/store/src/main/scala/docspell/store/records/RUserImap.scala @@ -0,0 +1,201 @@ +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.{MailConfig, SSLType} + +case class RUserImap( + id: Ident, + uid: Ident, + name: Ident, + imapHost: String, + imapPort: Option[Int], + imapUser: Option[String], + imapPassword: Option[Password], + imapSsl: SSLType, + imapCertCheck: Boolean, + created: Timestamp +) { + + def toMailConfig: MailConfig = { + val port = imapPort.map(p => s":$p").getOrElse("") + MailConfig( + s"imap://$imapHost$port", + imapUser.getOrElse(""), + imapPassword.map(_.pass).getOrElse(""), + imapSsl, + !imapCertCheck + ) + } +} + +object RUserImap { + + def apply[F[_]: Sync]( + uid: Ident, + name: Ident, + imapHost: String, + imapPort: Option[Int], + imapUser: Option[String], + imapPassword: Option[Password], + imapSsl: SSLType, + imapCertCheck: Boolean + ): F[RUserImap] = + for { + now <- Timestamp.current[F] + id <- Ident.randomId[F] + } yield RUserImap( + id, + uid, + name, + imapHost, + imapPort, + imapUser, + imapPassword, + imapSsl, + imapCertCheck, + now + ) + + def fromAccount( + accId: AccountId, + name: Ident, + imapHost: String, + imapPort: Option[Int], + imapUser: Option[String], + imapPassword: Option[Password], + imapSsl: SSLType, + imapCertCheck: Boolean + ): OptionT[ConnectionIO, RUserImap] = + for { + now <- OptionT.liftF(Timestamp.current[ConnectionIO]) + id <- OptionT.liftF(Ident.randomId[ConnectionIO]) + user <- OptionT(RUser.findByAccount(accId)) + } yield RUserImap( + id, + user.uid, + name, + imapHost, + imapPort, + imapUser, + imapPassword, + imapSsl, + imapCertCheck, + now + ) + + val table = fr"userimap" + + object Columns { + val id = Column("id") + val uid = Column("uid") + val name = Column("name") + val imapHost = Column("imap_host") + val imapPort = Column("imap_port") + val imapUser = Column("imap_user") + val imapPass = Column("imap_password") + val imapSsl = Column("imap_ssl") + val imapCertCheck = Column("imap_certcheck") + val created = Column("created") + + val all = List( + id, + uid, + name, + imapHost, + imapPort, + imapUser, + imapPass, + imapSsl, + imapCertCheck, + created + ) + } + + import Columns._ + + def insert(v: RUserImap): ConnectionIO[Int] = + insertRow( + table, + all, + sql"${v.id},${v.uid},${v.name},${v.imapHost},${v.imapPort},${v.imapUser},${v.imapPassword},${v.imapSsl},${v.imapCertCheck},${v.created}" + ).update.run + + def update(eId: Ident, v: RUserImap): ConnectionIO[Int] = + updateRow( + table, + id.is(eId), + commas( + name.setTo(v.name), + imapHost.setTo(v.imapHost), + imapPort.setTo(v.imapPort), + imapUser.setTo(v.imapUser), + imapPass.setTo(v.imapPassword), + imapSsl.setTo(v.imapSsl), + imapCertCheck.setTo(v.imapCertCheck) + ) + ).update.run + + def findByUser(userId: Ident): ConnectionIO[Vector[RUserImap]] = + selectSimple(all, table, uid.is(userId)).query[RUserImap].to[Vector] + + private def findByAccount0( + accId: AccountId, + nameQ: Option[String], + exact: Boolean + ): Query0[RUserImap] = { + 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[RUserImap] + } + + def findByAccount( + accId: AccountId, + nameQ: Option[String] + ): ConnectionIO[Vector[RUserImap]] = + findByAccount0(accId, nameQ, false).to[Vector] + + def getByName(accId: AccountId, name: Ident): ConnectionIO[Option[RUserImap]] = + 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) +} diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index ec4581ab..e1e74060 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -2,9 +2,11 @@ module Api exposing ( cancelJob , changePassword , checkCalEvent + , createImapSettings , createMailSettings , deleteAttachment , deleteEquip + , deleteImapSettings , deleteItem , deleteMailSettings , deleteOrg @@ -17,6 +19,7 @@ module Api exposing , getCollectiveSettings , getContacts , getEquipments + , getImapSettings , getInsights , getItemProposals , getJobQueueState @@ -81,6 +84,8 @@ import Api.Model.EmailSettingsList exposing (EmailSettingsList) import Api.Model.Equipment exposing (Equipment) import Api.Model.EquipmentList exposing (EquipmentList) import Api.Model.GenInvite exposing (GenInvite) +import Api.Model.ImapSettings exposing (ImapSettings) +import Api.Model.ImapSettingsList exposing (ImapSettingsList) import Api.Model.InviteResult exposing (InviteResult) import Api.Model.ItemDetail exposing (ItemDetail) import Api.Model.ItemInsights exposing (ItemInsights) @@ -259,7 +264,16 @@ sendMail flags opts receive = 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 + { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/smtp/" ++ name + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + +deleteImapSettings : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg +deleteImapSettings flags name receive = + Http2.authDelete + { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/imap/" ++ name , account = getAccount flags , expect = Http.expectJson receive Api.Model.BasicResult.decoder } @@ -268,12 +282,21 @@ deleteMailSettings flags name receive = 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 + { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/smtp?q=" ++ Url.percentEncode query , account = getAccount flags , expect = Http.expectJson receive Api.Model.EmailSettingsList.decoder } +getImapSettings : Flags -> String -> (Result Http.Error ImapSettingsList -> msg) -> Cmd msg +getImapSettings flags query receive = + Http2.authGet + { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/imap?q=" ++ Url.percentEncode query + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.ImapSettingsList.decoder + } + + createMailSettings : Flags -> Maybe String @@ -284,7 +307,7 @@ createMailSettings flags mname ems receive = case mname of Just en -> Http2.authPut - { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/" ++ en + { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/smtp/" ++ en , account = getAccount flags , body = Http.jsonBody (Api.Model.EmailSettings.encode ems) , expect = Http.expectJson receive Api.Model.BasicResult.decoder @@ -292,13 +315,38 @@ createMailSettings flags mname ems receive = Nothing -> Http2.authPost - { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings" + { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/smtp" , account = getAccount flags , body = Http.jsonBody (Api.Model.EmailSettings.encode ems) , expect = Http.expectJson receive Api.Model.BasicResult.decoder } +createImapSettings : + Flags + -> Maybe String + -> ImapSettings + -> (Result Http.Error BasicResult -> msg) + -> Cmd msg +createImapSettings flags mname ems receive = + case mname of + Just en -> + Http2.authPut + { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/imap/" ++ en + , account = getAccount flags + , body = Http.jsonBody (Api.Model.ImapSettings.encode ems) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + Nothing -> + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/email/settings/imap" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.ImapSettings.encode ems) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + --- Upload diff --git a/modules/webapp/src/main/elm/Comp/ImapSettingsForm.elm b/modules/webapp/src/main/elm/Comp/ImapSettingsForm.elm new file mode 100644 index 00000000..f2d58420 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ImapSettingsForm.elm @@ -0,0 +1,226 @@ +module Comp.ImapSettingsForm exposing + ( Model + , Msg + , emptyModel + , getSettings + , init + , isValid + , update + , view + ) + +import Api.Model.ImapSettings exposing (ImapSettings) +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 : ImapSettings + , name : String + , host : String + , portField : Comp.IntField.Model + , portNum : Maybe Int + , user : Maybe String + , passField : Comp.PasswordInput.Model + , password : Maybe String + , sslType : Comp.Dropdown.Model SSLType + , ignoreCertificates : Bool + } + + +emptyModel : Model +emptyModel = + { settings = Api.Model.ImapSettings.empty + , name = "" + , host = "" + , portField = Comp.IntField.init (Just 0) Nothing True "IMAP Port" + , portNum = Nothing + , user = Nothing + , passField = Comp.PasswordInput.init + , password = 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 : ImapSettings -> Model +init ems = + { settings = ems + , name = ems.name + , host = ems.imapHost + , portField = Comp.IntField.init (Just 0) Nothing True "IMAP Port" + , portNum = ems.imapPort + , user = ems.imapUser + , passField = Comp.PasswordInput.init + , password = ems.imapPassword + , 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, ImapSettings ) +getSettings model = + ( Util.Maybe.fromString model.settings.name + , { name = model.name + , imapHost = model.host + , imapUser = model.user + , imapPort = model.portNum + , imapPassword = model.password + , 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) + | 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 ) + + 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 "IMAP Host" ] + , input + [ type_ "text" + , placeholder "IMAP 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 "IMAP User" ] + , input + [ type_ "text" + , placeholder "IMAP Username, e.g. 'your.name@gmail.com'" + , Maybe.withDefault "" model.user |> value + , onInput SetUser + ] + [] + ] + , div [ class "field" ] + [ label [] [ text "IMAP Password" ] + , Html.map PassMsg (Comp.PasswordInput.view model.password model.passField) + ] + ] + , 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) + ] + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/ImapSettingsManage.elm b/modules/webapp/src/main/elm/Comp/ImapSettingsManage.elm new file mode 100644 index 00000000..5db98fb6 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ImapSettingsManage.elm @@ -0,0 +1,288 @@ +module Comp.ImapSettingsManage exposing + ( Model + , Msg + , emptyModel + , init + , update + , view + ) + +import Api +import Api.Model.BasicResult exposing (BasicResult) +import Api.Model.ImapSettings +import Api.Model.ImapSettingsList exposing (ImapSettingsList) +import Comp.ImapSettingsForm +import Comp.ImapSettingsTable +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.ImapSettingsTable.Model + , formModel : Comp.ImapSettingsForm.Model + , viewMode : ViewMode + , formError : Maybe String + , loading : Bool + , query : String + , deleteConfirm : Comp.YesNoDimmer.Model + } + + +emptyModel : Model +emptyModel = + { tableModel = Comp.ImapSettingsTable.emptyModel + , formModel = Comp.ImapSettingsForm.emptyModel + , viewMode = Table + , formError = Nothing + , loading = False + , query = "" + , deleteConfirm = Comp.YesNoDimmer.emptyModel + } + + +init : Flags -> ( Model, Cmd Msg ) +init flags = + ( emptyModel, Api.getImapSettings flags "" MailSettingsResp ) + + +type ViewMode + = Table + | Form + + +type Msg + = TableMsg Comp.ImapSettingsTable.Msg + | FormMsg Comp.ImapSettingsForm.Msg + | SetQuery String + | InitNew + | YesNoMsg Comp.YesNoDimmer.Msg + | RequestDelete + | SetViewMode ViewMode + | Submit + | SubmitResp (Result Http.Error BasicResult) + | LoadSettings + | MailSettingsResp (Result Http.Error ImapSettingsList) + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update flags msg model = + case msg of + InitNew -> + let + ems = + Api.Model.ImapSettings.empty + + nm = + { model + | viewMode = Form + , formError = Nothing + , formModel = Comp.ImapSettingsForm.init ems + } + in + ( nm, Cmd.none ) + + TableMsg m -> + let + ( tm, tc ) = + Comp.ImapSettingsTable.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.ImapSettingsForm.init ems + + Nothing -> + model.formModel + } + in + ( m2, Cmd.map TableMsg tc ) + + FormMsg m -> + let + ( fm, fc ) = + Comp.ImapSettingsForm.update m model.formModel + in + ( { model | formModel = fm }, Cmd.map FormMsg fc ) + + SetQuery str -> + let + m = + { model | query = str } + in + ( m, Api.getImapSettings flags str MailSettingsResp ) + + YesNoMsg m -> + let + ( dm, flag ) = + Comp.YesNoDimmer.update m model.deleteConfirm + + ( mid, _ ) = + Comp.ImapSettingsForm.getSettings model.formModel + + cmd = + case ( flag, mid ) of + ( True, Just name ) -> + Api.deleteImapSettings 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.ImapSettingsForm.getSettings model.formModel + + valid = + Comp.ImapSettingsForm.isValid model.formModel + in + if valid then + ( { model | loading = True }, Api.createImapSettings flags mid ems SubmitResp ) + + else + ( { model | formError = Just "Please fill required fields." }, Cmd.none ) + + LoadSettings -> + ( { model | loading = True }, Api.getImapSettings 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.ImapSettingsTable.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" ] + [ div [ class "horizontally 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 "item" ] + [ a + [ class "ui primary button" + , href "#" + , onClick InitNew + ] + [ i [ class "plus icon" ] [] + , text "New Settings" + ] + ] + ] + ] + , Html.map TableMsg (Comp.ImapSettingsTable.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.ImapSettingsForm.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" ] [] + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/ImapSettingsTable.elm b/modules/webapp/src/main/elm/Comp/ImapSettingsTable.elm new file mode 100644 index 00000000..a69bdc1e --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ImapSettingsTable.elm @@ -0,0 +1,74 @@ +module Comp.ImapSettingsTable exposing + ( Model + , Msg + , emptyModel + , init + , update + , view + ) + +import Api.Model.ImapSettings exposing (ImapSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick) + + +type alias Model = + { emailSettings : List ImapSettings + , selected : Maybe ImapSettings + } + + +emptyModel : Model +emptyModel = + init [] + + +init : List ImapSettings -> Model +init ems = + { emailSettings = ems + , selected = Nothing + } + + +type Msg + = Select ImapSettings + + +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" ] + ] + , tbody [] + (List.map (renderLine model) model.emailSettings) + ] + + +renderLine : Model -> ImapSettings -> Html Msg +renderLine model ems = + let + hostport = + case ems.imapPort of + Just p -> + ems.imapHost ++ ":" ++ String.fromInt p + + Nothing -> + ems.imapHost + in + tr + [ classList [ ( "active", model.selected == Just ems ) ] + , onClick (Select ems) + ] + [ td [ class "collapsible" ] [ text ems.name ] + , td [] [ text hostport ] + ] diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Data.elm b/modules/webapp/src/main/elm/Page/UserSettings/Data.elm index e8333c08..56276a09 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/Data.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/Data.elm @@ -7,6 +7,7 @@ module Page.UserSettings.Data exposing import Comp.ChangePasswordForm import Comp.EmailSettingsManage +import Comp.ImapSettingsManage import Comp.NotificationForm import Data.Flags exposing (Flags) @@ -15,6 +16,7 @@ type alias Model = { currentTab : Maybe Tab , changePassModel : Comp.ChangePasswordForm.Model , emailSettingsModel : Comp.EmailSettingsManage.Model + , imapSettingsModel : Comp.ImapSettingsManage.Model , notificationModel : Comp.NotificationForm.Model } @@ -24,6 +26,7 @@ emptyModel flags = { currentTab = Nothing , changePassModel = Comp.ChangePasswordForm.emptyModel , emailSettingsModel = Comp.EmailSettingsManage.emptyModel + , imapSettingsModel = Comp.ImapSettingsManage.emptyModel , notificationModel = Tuple.first (Comp.NotificationForm.init flags) } @@ -31,6 +34,7 @@ emptyModel flags = type Tab = ChangePassTab | EmailSettingsTab + | ImapSettingsTab | NotificationTab @@ -39,3 +43,4 @@ type Msg | ChangePassMsg Comp.ChangePasswordForm.Msg | EmailSettingsMsg Comp.EmailSettingsManage.Msg | NotificationMsg Comp.NotificationForm.Msg + | ImapSettingsMsg Comp.ImapSettingsManage.Msg diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm index 8dcf2f08..1a56cac1 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm @@ -2,6 +2,7 @@ module Page.UserSettings.Update exposing (update) import Comp.ChangePasswordForm import Comp.EmailSettingsManage +import Comp.ImapSettingsManage import Comp.NotificationForm import Data.Flags exposing (Flags) import Page.UserSettings.Data exposing (..) @@ -24,6 +25,13 @@ update flags msg model = in ( { m | emailSettingsModel = em }, Cmd.map EmailSettingsMsg c ) + ImapSettingsTab -> + let + ( em, c ) = + Comp.ImapSettingsManage.init flags + in + ( { m | imapSettingsModel = em }, Cmd.map ImapSettingsMsg c ) + ChangePassTab -> ( m, Cmd.none ) @@ -51,6 +59,13 @@ update flags msg model = in ( { model | emailSettingsModel = m2 }, Cmd.map EmailSettingsMsg c2 ) + ImapSettingsMsg m -> + let + ( m2, c2 ) = + Comp.ImapSettingsManage.update flags m model.imapSettingsModel + in + ( { model | imapSettingsModel = m2 }, Cmd.map ImapSettingsMsg c2 ) + NotificationMsg lm -> let ( m2, c2 ) = diff --git a/modules/webapp/src/main/elm/Page/UserSettings/View.elm b/modules/webapp/src/main/elm/Page/UserSettings/View.elm index 662dc425..bf02ba82 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/View.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/View.elm @@ -2,6 +2,7 @@ module Page.UserSettings.View exposing (view) import Comp.ChangePasswordForm import Comp.EmailSettingsManage +import Comp.ImapSettingsManage import Comp.NotificationForm import Html exposing (..) import Html.Attributes exposing (..) @@ -20,7 +21,8 @@ view model = , div [ class "ui attached fluid segment" ] [ div [ class "ui fluid vertical secondary menu" ] [ makeTab model ChangePassTab "Change Password" "user secret icon" - , makeTab model EmailSettingsTab "E-Mail Settings" "mail icon" + , makeTab model EmailSettingsTab "E-Mail Settings (SMTP)" "mail icon" + , makeTab model ImapSettingsTab "E-Mail Settings (IMAP)" "mail icon" , makeTab model NotificationTab "Notifications" "bullhorn icon" ] ] @@ -37,6 +39,9 @@ view model = Just NotificationTab -> viewNotificationForm model + Just ImapSettingsTab -> + viewImapSettings model + Nothing -> [] ) @@ -61,13 +66,25 @@ viewEmailSettings model = [ h2 [ class "ui header" ] [ i [ class "mail icon" ] [] , div [ class "content" ] - [ text "E-Mail Settings" + [ text "E-Mail Settings (Smtp)" ] ] , Html.map EmailSettingsMsg (Comp.EmailSettingsManage.view model.emailSettingsModel) ] +viewImapSettings : Model -> List (Html Msg) +viewImapSettings model = + [ h2 [ class "ui header" ] + [ i [ class "mail icon" ] [] + , div [ class "content" ] + [ text "E-Mail Settings (Imap)" + ] + ] + , Html.map ImapSettingsMsg (Comp.ImapSettingsManage.view model.imapSettingsModel) + ] + + viewChangePassword : Model -> List (Html Msg) viewChangePassword model = [ h2 [ class "ui header" ]