Prepare sending mail

This commit is contained in:
Eike Kettner 2020-01-08 22:44:34 +01:00
parent 51ce48997c
commit 7a3289c41d
12 changed files with 268 additions and 12 deletions

View File

@ -8,6 +8,7 @@ import emil.{MailAddress, SSLType}
import docspell.common._
import docspell.store._
import docspell.store.records.RUserEmail
import OMail.{ItemMail, SmtpSettings}
trait OMail[F[_]] {
@ -15,15 +16,31 @@ trait OMail[F[_]] {
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 deleteSettings(accId: AccountId, name: Ident): F[Int]
def sendMail(accId: AccountId, name: Ident, m: ItemMail): F[SendResult]
}
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(
name: Ident,
smtpHost: String,
@ -79,5 +96,8 @@ object OMail {
def deleteSettings(accId: AccountId, name: Ident): F[Int] =
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")))
})
}

View File

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

View File

@ -1348,7 +1348,7 @@ components:
type: string
addAllAttachments:
type: boolean
attachemntIds:
attachmentIds:
type: array
items:
type: string

View File

@ -70,6 +70,7 @@ object RestServer {
"attachment" -> AttachmentRoutes(restApp.backend, token),
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token),
"checkfile" -> CheckFileRoutes.secured(restApp.backend, token),
"email/send" -> MailSendRoutes(restApp.backend, token),
"email/settings" -> MailSettingsRoutes(restApp.backend, token)
)

View File

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

View File

@ -14,3 +14,17 @@ CREATE TABLE "useremail" (
unique ("uid", "name"),
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")
);

View File

@ -1,5 +1,6 @@
package docspell.store
import cats.implicits._
import emil._
import emil.javamail.syntax._
@ -29,6 +30,9 @@ object EmilUtil {
def unsafeReadMailAddress(str: String): MailAddress =
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 =
ma.asUnicodeString
}

View File

@ -93,6 +93,9 @@ trait DoobieMeta {
implicit val mailAddress: Meta[MailAddress] =
Meta[String].imap(EmilUtil.unsafeReadMailAddress)(EmilUtil.mailAddressString)
implicit def mailAddressList: Meta[List[MailAddress]] =
???
}
object DoobieMeta extends DoobieMeta {

View File

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

View File

@ -40,6 +40,7 @@ module Api exposing
, putUser
, refreshSession
, register
, sendMail
, setCollectiveSettings
, setConcEquip
, setConcPerson
@ -86,6 +87,7 @@ import Api.Model.Person exposing (Person)
import Api.Model.PersonList exposing (PersonList)
import Api.Model.ReferenceList exposing (ReferenceList)
import Api.Model.Registration exposing (Registration)
import Api.Model.SimpleMail exposing (SimpleMail)
import Api.Model.Source exposing (Source)
import Api.Model.SourceList exposing (SourceList)
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

View File

@ -33,6 +33,7 @@ import Html.Events exposing (onClick, onInput)
import Http
import Markdown
import Page exposing (Page(..))
import Util.Http
import Util.Maybe
import Util.Size
import Util.String
@ -60,6 +61,7 @@ type alias Model =
, dueDatePicker : DatePicker
, itemMail : Comp.ItemMail.Model
, mailOpen : Bool
, mailSendResult : Maybe BasicResult
}
@ -121,6 +123,7 @@ emptyModel =
, dueDatePicker = Comp.DatePicker.emptyModel
, itemMail = Comp.ItemMail.emptyModel
, mailOpen = False
, mailSendResult = Nothing
}
@ -165,6 +168,7 @@ type Msg
| RemoveDate
| ItemMailMsg Comp.ItemMail.Msg
| ToggleMail
| SendMailResp (Result Http.Error BasicResult)
@ -714,16 +718,49 @@ update key flags next msg model =
( { model
| itemMail = Comp.ItemMail.clear im
, mailOpen = False
, mailSendResult = Nothing
}
, Cmd.none
)
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 ->
( { 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
@ -793,7 +830,7 @@ view inav model =
, onClick ToggleMail
, href "#"
]
[ i [ class "mail icon" ] []
[ i [ class "mail outline icon" ] []
]
]
, renderMailForm model
@ -1258,4 +1295,23 @@ renderMailForm model =
[ text "Send this item via E-Mail"
]
, 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
]
]

View File

@ -42,8 +42,14 @@ type Msg
| Send
type alias MailInfo =
{ conn : String
, mail : SimpleMail
}
type FormAction
= FormSend SimpleMail
= FormSend MailInfo
| FormCancel
| FormNone
@ -132,14 +138,19 @@ update msg model =
( model, FormCancel )
Send ->
let
rec =
String.split "," model.receiver
case ( model.formError, Comp.Dropdown.getSelected model.connectionModel ) of
( Nothing, conn :: [] ) ->
let
rec =
String.split "," model.receiver
sm =
SimpleMail rec model.subject model.body model.attachAll []
in
( model, FormSend sm )
sm =
SimpleMail rec model.subject model.body model.attachAll []
in
( model, FormSend { conn = conn, mail = sm } )
_ ->
( model, FormNone )
isValid : Model -> Bool