mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-03-31 05:15:08 +00:00
Implement scan-mailbox form and routes
This commit is contained in:
parent
0d6677f90b
commit
6e8582ea80
@ -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
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -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"
|
||||
|
98
modules/webapp/src/main/elm/Comp/StringListInput.elm
Normal file
98
modules/webapp/src/main/elm/Comp/StringListInput.elm
Normal 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
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
@ -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"
|
||||
]
|
||||
|
Loading…
x
Reference in New Issue
Block a user