Implement scan-mailbox form and routes

This commit is contained in:
Eike Kettner 2020-05-18 11:43:18 +02:00
parent 0d6677f90b
commit 6e8582ea80
8 changed files with 452 additions and 8 deletions

View File

@ -11,6 +11,18 @@ import docspell.common._
trait OUserTask[F[_]] {
/** Return the settings for the scan-mailbox task of the current user.
* There is at most one such task per user.
*/
def getScanMailbox(account: AccountId): F[UserTask[ScanMailboxArgs]]
/** Updates the scan-mailbox tasks and notifies the joex nodes.
*/
def submitScanMailbox(
account: AccountId,
task: UserTask[ScanMailboxArgs]
): F[Unit]
/** Return the settings for the notify-due-items task of the current
* user. There is at most one such task per user.
*/
@ -51,6 +63,20 @@ object OUserTask {
_ <- joex.notifyAllNodes
} yield ()
def getScanMailbox(account: AccountId): F[UserTask[ScanMailboxArgs]] =
store
.getOneByName[ScanMailboxArgs](account, ScanMailboxArgs.taskName)
.getOrElseF(scanMailboxDefault(account))
def submitScanMailbox(
account: AccountId,
task: UserTask[ScanMailboxArgs]
): F[Unit] =
for {
_ <- store.updateOneTask[ScanMailboxArgs](account, task)
_ <- joex.notifyAllNodes
} yield ()
def getNotifyDueItems(account: AccountId): F[UserTask[NotifyDueItemsArgs]] =
store
.getOneByName[NotifyDueItemsArgs](account, NotifyDueItemsArgs.taskName)
@ -86,6 +112,27 @@ object OUserTask {
Nil
)
)
private def scanMailboxDefault(
account: AccountId
): F[UserTask[ScanMailboxArgs]] =
for {
id <- Ident.randomId[F]
} yield UserTask(
id,
ScanMailboxArgs.taskName,
false,
CalEvent.unsafe("*-*-* 0,12:00"),
ScanMailboxArgs(
account,
Ident.unsafe(""),
Nil,
Some(Duration.hours(12)),
None,
false,
None
)
)
})
}

View File

@ -13,6 +13,10 @@ case class Duration(nanos: Long) {
def seconds: Long = millis / 1000
def minutes: Long = seconds / 60
def hours: Long = minutes / 60
def toScala: FiniteDuration =
FiniteDuration(nanos, TimeUnit.NANOSECONDS)
@ -55,7 +59,6 @@ object Duration {
end = Timestamp.current[F]
} yield end.map(e => Duration.millis(e.toMillis - now.toMillis))
implicit val jsonEncoder: Encoder[Duration] =
Encoder.encodeLong.contramap(_.millis)

View File

@ -1825,6 +1825,7 @@ components:
- imapConnection
- schedule
- folders
- deleteMail
properties:
id:
type: string
@ -1843,6 +1844,7 @@ components:
format: calevent
receivedSinceHours:
type: integer
format: int32
description: |
Look only for mails newer than `receivedSinceHours' hours.
targetFolder:

View File

@ -76,6 +76,7 @@ object RestServer {
"email/settings" -> MailSettingsRoutes(restApp.backend, token),
"email/sent" -> SentMailRoutes(restApp.backend, token),
"usertask/notifydueitems" -> NotifyDueItemsRoutes(cfg, restApp.backend, token),
"usertask/scanmailbox" -> ScanMailboxRoutes(restApp.backend, token),
"calevent/check" -> CalEventCheckRoutes()
)

View File

@ -0,0 +1,103 @@
package docspell.restserver.routes
import cats.effect._
import cats.implicits._
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.circe.CirceEntityEncoder._
import org.http4s.circe.CirceEntityDecoder._
import docspell.backend.BackendApp
import docspell.backend.auth.AuthToken
import docspell.common._
import docspell.restapi.model._
import docspell.store.usertask._
import docspell.restserver.conv.Conversions
object ScanMailboxRoutes {
def apply[F[_]: Effect](
backend: BackendApp[F],
user: AuthToken
): HttpRoutes[F] = {
val dsl = new Http4sDsl[F] {}
val ut = backend.userTask
import dsl._
HttpRoutes.of {
case req @ POST -> Root / "startonce" =>
for {
data <- req.as[ScanMailboxSettings]
task = makeTask(user.account, data)
res <-
ut.executeNow(user.account, task)
.attempt
.map(Conversions.basicResult(_, "Submitted successfully."))
resp <- Ok(res)
} yield resp
case GET -> Root =>
for {
task <- ut.getScanMailbox(user.account)
res <- taskToSettings(user.account, backend, task)
resp <- Ok(res)
} yield resp
case req @ POST -> Root =>
for {
data <- req.as[ScanMailboxSettings]
task = makeTask(user.account, data)
res <-
ut.submitScanMailbox(user.account, task)
.attempt
.map(Conversions.basicResult(_, "Saved successfully."))
resp <- Ok(res)
} yield resp
}
}
def makeTask(
user: AccountId,
settings: ScanMailboxSettings
): UserTask[ScanMailboxArgs] =
UserTask(
settings.id,
ScanMailboxArgs.taskName,
settings.enabled,
settings.schedule,
ScanMailboxArgs(
user,
settings.imapConnection,
settings.folders,
settings.receivedSinceHours.map(_.toLong).map(Duration.hours),
settings.targetFolder,
settings.deleteMail,
settings.direction
)
)
def taskToSettings[F[_]: Sync](
account: AccountId,
backend: BackendApp[F],
task: UserTask[ScanMailboxArgs]
): F[ScanMailboxSettings] =
for {
conn <-
backend.mail
.getImapSettings(account, None)
.map(
_.find(_.name == task.args.imapConnection)
.map(_.name)
)
} yield ScanMailboxSettings(
task.id,
task.enabled,
conn.getOrElse(Ident.unsafe("")),
task.args.folders, //folders
task.timer,
task.args.receivedSince.map(_.hours.toInt),
task.args.targetFolder,
task.args.deleteMail,
task.args.direction
)
}

View File

@ -14,16 +14,18 @@ import Api.Model.Tag exposing (Tag)
import Api.Model.TagList exposing (TagList)
import Comp.CalEventInput
import Comp.Dropdown
import Comp.EmailInput
import Comp.IntField
import Comp.StringListInput
import Data.CalEvent exposing (CalEvent)
import Data.Direction exposing (Direction(..))
import Data.Flags exposing (Flags)
import Data.Validated exposing (Validated(..))
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onCheck, onClick)
import Html.Events exposing (onCheck, onClick, onInput)
import Http
import Util.Http
import Util.List
import Util.Maybe
import Util.Update
@ -32,6 +34,13 @@ type alias Model =
{ settings : ScanMailboxSettings
, connectionModel : Comp.Dropdown.Model String
, enabled : Bool
, deleteMail : Bool
, receivedHours : Maybe Int
, receivedHoursModel : Comp.IntField.Model
, targetFolder : Maybe String
, foldersModel : Comp.StringListInput.Model
, folders : List String
, direction : Maybe Direction
, schedule : Validated CalEvent
, scheduleModel : Comp.CalEventInput.Model
, formMsg : Maybe BasicResult
@ -44,10 +53,15 @@ type Msg
| ConnMsg (Comp.Dropdown.Msg String)
| ConnResp (Result Http.Error ImapSettingsList)
| ToggleEnabled
| ToggleDeleteMail
| CalEventMsg Comp.CalEventInput.Msg
| SetScanMailboxSettings (Result Http.Error ScanMailboxSettings)
| SubmitResp (Result Http.Error BasicResult)
| StartOnce
| ReceivedHoursMsg Comp.IntField.Msg
| SetTargetFolder String
| FoldersMsg Comp.StringListInput.Msg
| DirectionMsg (Maybe Direction)
initCmd : Flags -> Cmd Msg
@ -74,10 +88,17 @@ init flags =
, placeholder = "Select connection..."
}
, enabled = False
, deleteMail = False
, receivedHours = Nothing
, receivedHoursModel = Comp.IntField.init (Just 1) Nothing True "Received Since Hours"
, foldersModel = Comp.StringListInput.init
, folders = []
, targetFolder = Nothing
, direction = Nothing
, schedule = initialSchedule
, scheduleModel = sm
, formMsg = Nothing
, loading = 3
, loading = 2
}
, Cmd.batch
[ initCmd flags
@ -102,16 +123,29 @@ makeSettings model =
|> Maybe.map Valid
|> Maybe.withDefault (Invalid [ "Connection missing" ] "")
make smtp timer =
infolders =
if model.folders == [] then
Invalid [ "No folders given" ] []
else
Valid model.folders
make smtp timer folders =
{ prev
| imapConnection = smtp
, enabled = model.enabled
, receivedSinceHours = model.receivedHours
, deleteMail = model.deleteMail
, targetFolder = model.targetFolder
, folders = folders
, direction = Maybe.map Data.Direction.toString model.direction
, schedule = Data.CalEvent.makeEvent timer
}
in
Data.Validated.map2 make
Data.Validated.map3 make
conn
model.schedule
infolders
withValidSettings : (ScanMailboxSettings -> Cmd Msg) -> Model -> ( Model, Cmd Msg )
@ -211,6 +245,60 @@ update flags msg model =
, Cmd.none
)
ToggleDeleteMail ->
( { model
| deleteMail = not model.deleteMail
, formMsg = Nothing
}
, Cmd.none
)
ReceivedHoursMsg m ->
let
( pm, val ) =
Comp.IntField.update m model.receivedHoursModel
in
( { model
| receivedHoursModel = pm
, receivedHours = val
, formMsg = Nothing
}
, Cmd.none
)
SetTargetFolder str ->
( { model | targetFolder = Util.Maybe.fromString str }
, Cmd.none
)
FoldersMsg lm ->
let
( fm, itemAction ) =
Comp.StringListInput.update lm model.foldersModel
newList =
case itemAction of
Comp.StringListInput.AddAction s ->
Util.List.distinct (s :: model.folders)
Comp.StringListInput.RemoveAction s ->
List.filter (\e -> e /= s) model.folders
Comp.StringListInput.NoAction ->
model.folders
in
( { model
| foldersModel = fm
, folders = newList
}
, Cmd.none
)
DirectionMsg md ->
( { model | direction = md }
, Cmd.none
)
SetScanMailboxSettings (Ok s) ->
let
imap =
@ -234,7 +322,12 @@ update flags msg model =
( { nm
| settings = s
, enabled = s.enabled
, deleteMail = s.deleteMail
, receivedHours = s.receivedSinceHours
, targetFolder = s.targetFolder
, folders = s.folders
, schedule = Data.Validated.Unknown newSchedule
, direction = Maybe.andThen Data.Direction.fromString s.direction
, scheduleModel = sm
, formMsg = Nothing
, loading = model.loading - 1
@ -313,13 +406,110 @@ view extraClasses model =
[ text "Loading..."
]
]
, div [ class "inline field" ]
[ div [ class "ui checkbox" ]
[ input
[ type_ "checkbox"
, onCheck (\_ -> ToggleEnabled)
, checked model.enabled
]
[]
, label [] [ text "Enabled" ]
]
, span [ class "small-info" ]
[ text "Enable or disable this task."
]
]
, div [ class "required field" ]
[ label [] [ text "Send via" ]
[ label [] [ text "Mailbox" ]
, Html.map ConnMsg (Comp.Dropdown.view model.connectionModel)
, span [ class "small-info" ]
[ text "The IMAP connection to use when sending notification mails."
]
]
, div [ class "required field" ]
[ label [] [ text "Folders" ]
, Html.map FoldersMsg (Comp.StringListInput.view model.folders model.foldersModel)
, span [ class "small-info" ]
[ text "The folders to go through"
]
]
, Html.map ReceivedHoursMsg
(Comp.IntField.viewWithInfo
"Select mails newer than `now - receivedHours`"
model.receivedHours
"field"
model.receivedHoursModel
)
, div [ class "field" ]
[ label [] [ text "Target folder" ]
, input
[ type_ "text"
, onInput SetTargetFolder
, Maybe.withDefault "" model.targetFolder |> value
]
[]
, span [ class "small-info" ]
[ text "Move all mails successfully submitted into this folder."
]
]
, div [ class "inline field" ]
[ div [ class "ui checkbox" ]
[ input
[ type_ "checkbox"
, onCheck (\_ -> ToggleDeleteMail)
, checked model.deleteMail
]
[]
, label [] [ text "Delete imported mails" ]
]
, span [ class "small-info" ]
[ text "Whether to delete all mails successfully imported into docspell."
]
]
, div [ class "required field" ]
[ label [] [ text "Item direction" ]
, div [ class "grouped fields" ]
[ div [ class "field" ]
[ div [ class "ui radio checkbox" ]
[ input
[ type_ "radio"
, checked (model.direction == Nothing)
, onCheck (\_ -> DirectionMsg Nothing)
]
[]
, label [] [ text "Automatic" ]
]
]
, div [ class "field" ]
[ div [ class "ui radio checkbox" ]
[ input
[ type_ "radio"
, checked (model.direction == Just Incoming)
, onCheck (\_ -> DirectionMsg (Just Incoming))
]
[]
, label [] [ text "Incoming" ]
]
]
, div [ class "field" ]
[ div [ class "ui radio checkbox" ]
[ input
[ type_ "radio"
, checked (model.direction == Just Outgoing)
, onCheck (\_ -> DirectionMsg (Just Outgoing))
]
[]
, label [] [ text "Outgoing" ]
]
]
, span [ class "small-info" ]
[ text "Sets the direction for an item. If you know all mails are incoming or "
, text "outgoing, you can set it here. Otherwise it will be guessed from looking "
, text "at sender and receiver."
]
]
]
, div [ class "required field" ]
[ label []
[ text "Schedule"

View File

@ -0,0 +1,98 @@
module Comp.StringListInput exposing
( ItemAction(..)
, Model
, Msg
, init
, update
, view
)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)
import Util.Maybe
type alias Model =
{ currentInput : String
}
type Msg
= AddString
| RemoveString String
| SetString String
init : Model
init =
{ currentInput = ""
}
--- Update
type ItemAction
= AddAction String
| RemoveAction String
| NoAction
update : Msg -> Model -> ( Model, ItemAction )
update msg model =
case msg of
SetString str ->
( { model | currentInput = str }
, NoAction
)
AddString ->
( { model | currentInput = "" }
, Util.Maybe.fromString model.currentInput
|> Maybe.map AddAction
|> Maybe.withDefault NoAction
)
RemoveString s ->
( model, RemoveAction s )
--- View
view : List String -> Model -> Html Msg
view values model =
let
valueItem s =
div [ class "item" ]
[ a
[ class "ui icon link"
, onClick (RemoveString s)
, href "#"
]
[ i [ class "delete icon" ] []
]
, text s
]
in
div [ class "string-list-input" ]
[ div [ class "ui list" ]
(List.map valueItem values)
, div [ class "ui icon input" ]
[ input
[ placeholder ""
, type_ "text"
, onInput SetString
, value model.currentInput
]
[]
, i
[ class "circular add link icon"
, onClick AddString
]
[]
]
]

View File

@ -128,7 +128,7 @@ viewNotificationForm model =
viewScanMailboxForm : Model -> List (Html Msg)
viewScanMailboxForm model =
[ h2 [ class "ui header" ]
[ i [ class "ui bullhorn icon" ] []
[ i [ class "ui envelope open outline icon" ] []
, div [ class "content" ]
[ text "Scan Mailbox"
]