Add imap settings

This commit is contained in:
Eike Kettner 2020-05-05 22:48:08 +02:00
parent 2a5ffd391e
commit c9de74fd91
14 changed files with 1221 additions and 38 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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`)
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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