mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-02 13:32:51 +00:00
Prepare sending mail
This commit is contained in:
parent
51ce48997c
commit
7a3289c41d
@ -8,6 +8,7 @@ import emil.{MailAddress, SSLType}
|
|||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.store._
|
import docspell.store._
|
||||||
import docspell.store.records.RUserEmail
|
import docspell.store.records.RUserEmail
|
||||||
|
import OMail.{ItemMail, SmtpSettings}
|
||||||
|
|
||||||
trait OMail[F[_]] {
|
trait OMail[F[_]] {
|
||||||
|
|
||||||
@ -15,15 +16,31 @@ trait OMail[F[_]] {
|
|||||||
|
|
||||||
def findSettings(accId: AccountId, name: Ident): OptionT[F, RUserEmail]
|
def findSettings(accId: AccountId, name: Ident): OptionT[F, RUserEmail]
|
||||||
|
|
||||||
def createSettings(accId: AccountId, data: OMail.SmtpSettings): F[AddResult]
|
def createSettings(accId: AccountId, data: SmtpSettings): F[AddResult]
|
||||||
|
|
||||||
def updateSettings(accId: AccountId, name: Ident, data: OMail.SmtpSettings): F[Int]
|
def updateSettings(accId: AccountId, name: Ident, data: OMail.SmtpSettings): F[Int]
|
||||||
|
|
||||||
def deleteSettings(accId: AccountId, name: Ident): F[Int]
|
def deleteSettings(accId: AccountId, name: Ident): F[Int]
|
||||||
|
|
||||||
|
def sendMail(accId: AccountId, name: Ident, m: ItemMail): F[SendResult]
|
||||||
}
|
}
|
||||||
|
|
||||||
object OMail {
|
object OMail {
|
||||||
|
|
||||||
|
case class ItemMail(
|
||||||
|
item: Ident,
|
||||||
|
subject: String,
|
||||||
|
recipients: List[MailAddress],
|
||||||
|
body: String,
|
||||||
|
attach: AttachSelection
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed trait AttachSelection
|
||||||
|
object AttachSelection {
|
||||||
|
case object All extends AttachSelection
|
||||||
|
case class Selected(ids: List[Ident]) extends AttachSelection
|
||||||
|
}
|
||||||
|
|
||||||
case class SmtpSettings(
|
case class SmtpSettings(
|
||||||
name: Ident,
|
name: Ident,
|
||||||
smtpHost: String,
|
smtpHost: String,
|
||||||
@ -79,5 +96,8 @@ object OMail {
|
|||||||
|
|
||||||
def deleteSettings(accId: AccountId, name: Ident): F[Int] =
|
def deleteSettings(accId: AccountId, name: Ident): F[Int] =
|
||||||
store.transact(RUserEmail.delete(accId, name))
|
store.transact(RUserEmail.delete(accId, name))
|
||||||
|
|
||||||
|
def sendMail(accId: AccountId, name: Ident, m: ItemMail): F[SendResult] =
|
||||||
|
Effect[F].pure(SendResult.Failure(new Exception("not implemented")))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
package docspell.backend.ops
|
||||||
|
|
||||||
|
import docspell.common._
|
||||||
|
|
||||||
|
sealed trait SendResult
|
||||||
|
|
||||||
|
object SendResult {
|
||||||
|
|
||||||
|
case class Success(id: Ident) extends SendResult
|
||||||
|
|
||||||
|
case class Failure(ex: Throwable) extends SendResult
|
||||||
|
}
|
@ -1348,7 +1348,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
addAllAttachments:
|
addAllAttachments:
|
||||||
type: boolean
|
type: boolean
|
||||||
attachemntIds:
|
attachmentIds:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
@ -70,6 +70,7 @@ object RestServer {
|
|||||||
"attachment" -> AttachmentRoutes(restApp.backend, token),
|
"attachment" -> AttachmentRoutes(restApp.backend, token),
|
||||||
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token),
|
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token),
|
||||||
"checkfile" -> CheckFileRoutes.secured(restApp.backend, token),
|
"checkfile" -> CheckFileRoutes.secured(restApp.backend, token),
|
||||||
|
"email/send" -> MailSendRoutes(restApp.backend, token),
|
||||||
"email/settings" -> MailSettingsRoutes(restApp.backend, token)
|
"email/settings" -> MailSettingsRoutes(restApp.backend, token)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
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.backend.ops.OMail.{AttachSelection, ItemMail}
|
||||||
|
import docspell.backend.ops.SendResult
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.restapi.model._
|
||||||
|
import docspell.store.EmilUtil
|
||||||
|
|
||||||
|
object MailSendRoutes {
|
||||||
|
|
||||||
|
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||||
|
val dsl = new Http4sDsl[F] {}
|
||||||
|
import dsl._
|
||||||
|
|
||||||
|
HttpRoutes.of {
|
||||||
|
case req @ POST -> Root / Ident(name) / Ident(id) =>
|
||||||
|
for {
|
||||||
|
in <- req.as[SimpleMail]
|
||||||
|
mail = convertIn(id, in)
|
||||||
|
res <- mail.traverse(m => backend.mail.sendMail(user.account, name, m))
|
||||||
|
resp <- res.fold(
|
||||||
|
err => Ok(BasicResult(false, s"Invalid mail data: $err")),
|
||||||
|
res => Ok(convertOut(res))
|
||||||
|
)
|
||||||
|
} yield resp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def convertIn(item: Ident, s: SimpleMail): Either[String, ItemMail] =
|
||||||
|
for {
|
||||||
|
rec <- s.recipients.traverse(EmilUtil.readMailAddress)
|
||||||
|
fileIds <- s.attachmentIds.traverse(Ident.fromString)
|
||||||
|
sel = if (s.addAllAttachments) AttachSelection.All else AttachSelection.Selected(fileIds)
|
||||||
|
} yield ItemMail(item, s.subject, rec, s.body, sel)
|
||||||
|
|
||||||
|
def convertOut(res: SendResult): BasicResult =
|
||||||
|
res match {
|
||||||
|
case SendResult.Success(_) =>
|
||||||
|
BasicResult(true, "Mail sent.")
|
||||||
|
case SendResult.Failure(ex) =>
|
||||||
|
BasicResult(false, s"Mail sending failed: ${ex.getMessage}")
|
||||||
|
}
|
||||||
|
}
|
@ -14,3 +14,17 @@ CREATE TABLE "useremail" (
|
|||||||
unique ("uid", "name"),
|
unique ("uid", "name"),
|
||||||
foreign key ("uid") references "user_"("uid")
|
foreign key ("uid") references "user_"("uid")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "sentmail" (
|
||||||
|
"id" varchar(254) not null primary key,
|
||||||
|
"uid" varchar(254) not null,
|
||||||
|
"item_id" varchar(254) not null,
|
||||||
|
"message_id" varchar(254) not null,
|
||||||
|
"sender" varchar(254) not null,
|
||||||
|
"subject" varchar(254) not null,
|
||||||
|
"recipients" varchar(254) not null,
|
||||||
|
"body" text not null,
|
||||||
|
"created" timestamp not null,
|
||||||
|
foreign key("uid") references "user_"("uid"),
|
||||||
|
foreign key("item_id") references "item"("itemid")
|
||||||
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package docspell.store
|
package docspell.store
|
||||||
|
|
||||||
|
import cats.implicits._
|
||||||
import emil._
|
import emil._
|
||||||
import emil.javamail.syntax._
|
import emil.javamail.syntax._
|
||||||
|
|
||||||
@ -29,6 +30,9 @@ object EmilUtil {
|
|||||||
def unsafeReadMailAddress(str: String): MailAddress =
|
def unsafeReadMailAddress(str: String): MailAddress =
|
||||||
readMailAddress(str).fold(sys.error, identity)
|
readMailAddress(str).fold(sys.error, identity)
|
||||||
|
|
||||||
|
def readMultipleAddresses(str: String): Either[String, List[MailAddress]] =
|
||||||
|
str.split(',').toList.map(_.trim).traverse(readMailAddress)
|
||||||
|
|
||||||
def mailAddressString(ma: MailAddress): String =
|
def mailAddressString(ma: MailAddress): String =
|
||||||
ma.asUnicodeString
|
ma.asUnicodeString
|
||||||
}
|
}
|
||||||
|
@ -93,6 +93,9 @@ trait DoobieMeta {
|
|||||||
|
|
||||||
implicit val mailAddress: Meta[MailAddress] =
|
implicit val mailAddress: Meta[MailAddress] =
|
||||||
Meta[String].imap(EmilUtil.unsafeReadMailAddress)(EmilUtil.mailAddressString)
|
Meta[String].imap(EmilUtil.unsafeReadMailAddress)(EmilUtil.mailAddressString)
|
||||||
|
|
||||||
|
implicit def mailAddressList: Meta[List[MailAddress]] =
|
||||||
|
???
|
||||||
}
|
}
|
||||||
|
|
||||||
object DoobieMeta extends DoobieMeta {
|
object DoobieMeta extends DoobieMeta {
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
package docspell.store.records
|
||||||
|
|
||||||
|
import fs2.Stream
|
||||||
|
import doobie._
|
||||||
|
import doobie.implicits._
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.store.impl.Column
|
||||||
|
import docspell.store.impl.Implicits._
|
||||||
|
import emil.MailAddress
|
||||||
|
|
||||||
|
case class RSentMail(
|
||||||
|
id: Ident,
|
||||||
|
uid: Ident,
|
||||||
|
itemId: Ident,
|
||||||
|
messageId: String,
|
||||||
|
sender: MailAddress,
|
||||||
|
subject: String,
|
||||||
|
recipients: List[MailAddress],
|
||||||
|
body: String,
|
||||||
|
created: Timestamp
|
||||||
|
) {}
|
||||||
|
|
||||||
|
object RSentMail {
|
||||||
|
|
||||||
|
val table = fr"sentmail"
|
||||||
|
|
||||||
|
object Columns {
|
||||||
|
val id = Column("id")
|
||||||
|
val uid = Column("uid")
|
||||||
|
val itemId = Column("item_id")
|
||||||
|
val messageId = Column("message_id")
|
||||||
|
val sender = Column("sender")
|
||||||
|
val subject = Column("subject")
|
||||||
|
val recipients = Column("recipients")
|
||||||
|
val body = Column("body")
|
||||||
|
val created = Column("created")
|
||||||
|
|
||||||
|
val all = List(
|
||||||
|
id,
|
||||||
|
uid,
|
||||||
|
itemId,
|
||||||
|
messageId,
|
||||||
|
sender,
|
||||||
|
subject,
|
||||||
|
recipients,
|
||||||
|
body,
|
||||||
|
created
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
import Columns._
|
||||||
|
|
||||||
|
def insert(v: RSentMail): ConnectionIO[Int] =
|
||||||
|
insertRow(
|
||||||
|
table,
|
||||||
|
all,
|
||||||
|
sql"${v.id},${v.uid},${v.itemId},${v.messageId},${v.sender},${v.subject},${v.recipients},${v.body},${v.created}"
|
||||||
|
).update.run
|
||||||
|
|
||||||
|
def findByUser(userId: Ident): Stream[ConnectionIO, RSentMail] =
|
||||||
|
selectSimple(all, table, uid.is(userId)).query[RSentMail].stream
|
||||||
|
|
||||||
|
}
|
@ -40,6 +40,7 @@ module Api exposing
|
|||||||
, putUser
|
, putUser
|
||||||
, refreshSession
|
, refreshSession
|
||||||
, register
|
, register
|
||||||
|
, sendMail
|
||||||
, setCollectiveSettings
|
, setCollectiveSettings
|
||||||
, setConcEquip
|
, setConcEquip
|
||||||
, setConcPerson
|
, setConcPerson
|
||||||
@ -86,6 +87,7 @@ import Api.Model.Person exposing (Person)
|
|||||||
import Api.Model.PersonList exposing (PersonList)
|
import Api.Model.PersonList exposing (PersonList)
|
||||||
import Api.Model.ReferenceList exposing (ReferenceList)
|
import Api.Model.ReferenceList exposing (ReferenceList)
|
||||||
import Api.Model.Registration exposing (Registration)
|
import Api.Model.Registration exposing (Registration)
|
||||||
|
import Api.Model.SimpleMail exposing (SimpleMail)
|
||||||
import Api.Model.Source exposing (Source)
|
import Api.Model.Source exposing (Source)
|
||||||
import Api.Model.SourceList exposing (SourceList)
|
import Api.Model.SourceList exposing (SourceList)
|
||||||
import Api.Model.Tag exposing (Tag)
|
import Api.Model.Tag exposing (Tag)
|
||||||
@ -105,6 +107,24 @@ import Util.Http as Http2
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--- Mail Send
|
||||||
|
|
||||||
|
|
||||||
|
sendMail :
|
||||||
|
Flags
|
||||||
|
-> { conn : String, item : String, mail : SimpleMail }
|
||||||
|
-> (Result Http.Error BasicResult -> msg)
|
||||||
|
-> Cmd msg
|
||||||
|
sendMail flags opts receive =
|
||||||
|
Http2.authPost
|
||||||
|
{ url = flags.config.baseUrl ++ "/api/v1/sec/email/send/" ++ opts.conn ++ "/" ++ opts.item
|
||||||
|
, account = getAccount flags
|
||||||
|
, body = Http.jsonBody (Api.Model.SimpleMail.encode opts.mail)
|
||||||
|
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--- Mail Settings
|
--- Mail Settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ import Html.Events exposing (onClick, onInput)
|
|||||||
import Http
|
import Http
|
||||||
import Markdown
|
import Markdown
|
||||||
import Page exposing (Page(..))
|
import Page exposing (Page(..))
|
||||||
|
import Util.Http
|
||||||
import Util.Maybe
|
import Util.Maybe
|
||||||
import Util.Size
|
import Util.Size
|
||||||
import Util.String
|
import Util.String
|
||||||
@ -60,6 +61,7 @@ type alias Model =
|
|||||||
, dueDatePicker : DatePicker
|
, dueDatePicker : DatePicker
|
||||||
, itemMail : Comp.ItemMail.Model
|
, itemMail : Comp.ItemMail.Model
|
||||||
, mailOpen : Bool
|
, mailOpen : Bool
|
||||||
|
, mailSendResult : Maybe BasicResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -121,6 +123,7 @@ emptyModel =
|
|||||||
, dueDatePicker = Comp.DatePicker.emptyModel
|
, dueDatePicker = Comp.DatePicker.emptyModel
|
||||||
, itemMail = Comp.ItemMail.emptyModel
|
, itemMail = Comp.ItemMail.emptyModel
|
||||||
, mailOpen = False
|
, mailOpen = False
|
||||||
|
, mailSendResult = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -165,6 +168,7 @@ type Msg
|
|||||||
| RemoveDate
|
| RemoveDate
|
||||||
| ItemMailMsg Comp.ItemMail.Msg
|
| ItemMailMsg Comp.ItemMail.Msg
|
||||||
| ToggleMail
|
| ToggleMail
|
||||||
|
| SendMailResp (Result Http.Error BasicResult)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -714,16 +718,49 @@ update key flags next msg model =
|
|||||||
( { model
|
( { model
|
||||||
| itemMail = Comp.ItemMail.clear im
|
| itemMail = Comp.ItemMail.clear im
|
||||||
, mailOpen = False
|
, mailOpen = False
|
||||||
|
, mailSendResult = Nothing
|
||||||
}
|
}
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
Comp.ItemMail.FormSend sm ->
|
Comp.ItemMail.FormSend sm ->
|
||||||
Debug.todo "implement send"
|
let
|
||||||
|
mail =
|
||||||
|
{ item = model.item.id
|
||||||
|
, mail = sm.mail
|
||||||
|
, conn = sm.conn
|
||||||
|
}
|
||||||
|
in
|
||||||
|
( model, Api.sendMail flags mail SendMailResp )
|
||||||
|
|
||||||
ToggleMail ->
|
ToggleMail ->
|
||||||
( { model | mailOpen = not model.mailOpen }, Cmd.none )
|
( { model | mailOpen = not model.mailOpen }, Cmd.none )
|
||||||
|
|
||||||
|
SendMailResp (Ok br) ->
|
||||||
|
let
|
||||||
|
mm =
|
||||||
|
if br.success then
|
||||||
|
Comp.ItemMail.clear model.itemMail
|
||||||
|
|
||||||
|
else
|
||||||
|
model.itemMail
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| itemMail = mm
|
||||||
|
, mailSendResult = Just br
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
SendMailResp (Err err) ->
|
||||||
|
let
|
||||||
|
errmsg =
|
||||||
|
Util.Http.errorToString err
|
||||||
|
in
|
||||||
|
( { model | mailSendResult = Just (BasicResult False errmsg) }
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- view
|
-- view
|
||||||
@ -793,7 +830,7 @@ view inav model =
|
|||||||
, onClick ToggleMail
|
, onClick ToggleMail
|
||||||
, href "#"
|
, href "#"
|
||||||
]
|
]
|
||||||
[ i [ class "mail icon" ] []
|
[ i [ class "mail outline icon" ] []
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
, renderMailForm model
|
, renderMailForm model
|
||||||
@ -1258,4 +1295,23 @@ renderMailForm model =
|
|||||||
[ text "Send this item via E-Mail"
|
[ text "Send this item via E-Mail"
|
||||||
]
|
]
|
||||||
, Html.map ItemMailMsg (Comp.ItemMail.view model.itemMail)
|
, Html.map ItemMailMsg (Comp.ItemMail.view model.itemMail)
|
||||||
|
, div
|
||||||
|
[ classList
|
||||||
|
[ ( "ui message", True )
|
||||||
|
, ( "error"
|
||||||
|
, Maybe.map .success model.mailSendResult
|
||||||
|
|> Maybe.map not
|
||||||
|
|> Maybe.withDefault False
|
||||||
|
)
|
||||||
|
, ( "success"
|
||||||
|
, Maybe.map .success model.mailSendResult
|
||||||
|
|> Maybe.withDefault False
|
||||||
|
)
|
||||||
|
, ( "invisible hidden", model.mailSendResult == Nothing )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ Maybe.map .message model.mailSendResult
|
||||||
|
|> Maybe.withDefault ""
|
||||||
|
|> text
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
@ -42,8 +42,14 @@ type Msg
|
|||||||
| Send
|
| Send
|
||||||
|
|
||||||
|
|
||||||
|
type alias MailInfo =
|
||||||
|
{ conn : String
|
||||||
|
, mail : SimpleMail
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
type FormAction
|
type FormAction
|
||||||
= FormSend SimpleMail
|
= FormSend MailInfo
|
||||||
| FormCancel
|
| FormCancel
|
||||||
| FormNone
|
| FormNone
|
||||||
|
|
||||||
@ -132,14 +138,19 @@ update msg model =
|
|||||||
( model, FormCancel )
|
( model, FormCancel )
|
||||||
|
|
||||||
Send ->
|
Send ->
|
||||||
let
|
case ( model.formError, Comp.Dropdown.getSelected model.connectionModel ) of
|
||||||
rec =
|
( Nothing, conn :: [] ) ->
|
||||||
String.split "," model.receiver
|
let
|
||||||
|
rec =
|
||||||
|
String.split "," model.receiver
|
||||||
|
|
||||||
sm =
|
sm =
|
||||||
SimpleMail rec model.subject model.body model.attachAll []
|
SimpleMail rec model.subject model.body model.attachAll []
|
||||||
in
|
in
|
||||||
( model, FormSend sm )
|
( model, FormSend { conn = conn, mail = sm } )
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
( model, FormNone )
|
||||||
|
|
||||||
|
|
||||||
isValid : Model -> Bool
|
isValid : Model -> Bool
|
||||||
|
Loading…
x
Reference in New Issue
Block a user