feat(communication): support attachments in course/tutorial comm's
This commit is contained in:
parent
d68588037f
commit
5bd9ea85e8
@ -17,6 +17,8 @@ RGTutorialParticipants tutn@TutorialName: Tutorium-Teilnehmer:innen (#{tutn})
|
|||||||
RGExamRegistered examn@ExamName: Angemeldet zur Prüfung „#{examn}“
|
RGExamRegistered examn@ExamName: Angemeldet zur Prüfung „#{examn}“
|
||||||
RGSheetSubmittor shn@SheetName: Abgebende für das Übungsblatt „#{shn}“
|
RGSheetSubmittor shn@SheetName: Abgebende für das Übungsblatt „#{shn}“
|
||||||
CommSubject: Betreff
|
CommSubject: Betreff
|
||||||
|
CommAttachments: Anhänge
|
||||||
|
CommAttachmentsTip: Im Allgemeinen ist es vorzuziehen Dateien, die Sie mit den Empfängern teilen möchten, als Material hochzuladen (und ggf. in der Nachricht zu verlinken). So ist die Datei für die Empfänger dauerhaft abrufbar und auch Personen, die sich z.B. erst später zum Kurs anmelden, haben Zugriff auf die Datei.
|
||||||
CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt
|
CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt
|
||||||
CommTestSuccess: Nachricht wurde zu Testzwecken nur an Sie selbst versandt
|
CommTestSuccess: Nachricht wurde zu Testzwecken nur an Sie selbst versandt
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,8 @@ RGTutorialParticipants tutn: Tutorial participants (#{tutn})
|
|||||||
RGExamRegistered examn: Registered for exam “#{examn}”
|
RGExamRegistered examn: Registered for exam “#{examn}”
|
||||||
RGSheetSubmittor shn: Submitted for exercise sheet “#{shn}”
|
RGSheetSubmittor shn: Submitted for exercise sheet “#{shn}”
|
||||||
CommSubject: Subject
|
CommSubject: Subject
|
||||||
|
CommAttachments: Attachments
|
||||||
|
CommAttachmentsTip: In general it is preferable to upload files as course material instead of sending them as attachments. You can then link to the material from the message. The file is then permanently accessable to the recipients and to persons that, for example, register for the Course at a later date.
|
||||||
CommSuccess n: Message was sent to #{n} #{pluralEN n "recipient" "recipients"}
|
CommSuccess n: Message was sent to #{n} #{pluralEN n "recipient" "recipients"}
|
||||||
CommTestSuccess: Message was sent only to yourself for testing purposes
|
CommTestSuccess: Message was sent only to yourself for testing purposes
|
||||||
|
|
||||||
|
|||||||
@ -78,16 +78,16 @@ data CommunicationRoute = CommunicationRoute
|
|||||||
|
|
||||||
data Communication = Communication
|
data Communication = Communication
|
||||||
{ cRecipients :: Set (Either UserEmail UserId)
|
{ cRecipients :: Set (Either UserEmail UserId)
|
||||||
, cSubject :: Maybe Text
|
, cContent :: CommunicationContent
|
||||||
, cBody :: Html
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
makeLenses_ ''Communication
|
||||||
|
|
||||||
|
|
||||||
crJobsCourseCommunication, crTestJobsCourseCommunication :: CourseId -> Communication -> ConduitT () Job (YesodDB UniWorX) ()
|
crJobsCourseCommunication, crTestJobsCourseCommunication :: CourseId -> Communication -> ConduitT () Job (YesodDB UniWorX) ()
|
||||||
crJobsCourseCommunication jCourse Communication{..} = do
|
crJobsCourseCommunication jCourse Communication{..} = do
|
||||||
jSender <- requireAuthId
|
jSender <- requireAuthId
|
||||||
let jSubject = cSubject
|
let jMailContent = cContent
|
||||||
jMailContent = cBody
|
|
||||||
allRecipients = Set.toList $ Set.insert (Right jSender) cRecipients
|
allRecipients = Set.toList $ Set.insert (Right jSender) cRecipients
|
||||||
jMailObjectUUID <- liftIO getRandom
|
jMailObjectUUID <- liftIO getRandom
|
||||||
jAllRecipientAddresses <- lift . fmap Set.fromList . forM allRecipients $ \case
|
jAllRecipientAddresses <- lift . fmap Set.fromList . forM allRecipients $ \case
|
||||||
@ -99,7 +99,7 @@ crTestJobsCourseCommunication jCourse comm = do
|
|||||||
jSender <- requireAuthId
|
jSender <- requireAuthId
|
||||||
|
|
||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
let comm' = comm { cSubject = Just . mr . MsgCommCourseTestSubject . fromMaybe (mr MsgUtilCommCourseSubject) $ cSubject comm }
|
let comm' = comm & _cContent . _ccSubject %~ Just . mr . MsgCommCourseTestSubject . fromMaybe (mr MsgUtilCommCourseSubject)
|
||||||
crJobsCourseCommunication jCourse comm' .| C.filter ((== Right jSender) . jRecipientEmail)
|
crJobsCourseCommunication jCourse comm' .| C.filter ((== Right jSender) . jRecipientEmail)
|
||||||
|
|
||||||
|
|
||||||
@ -209,8 +209,11 @@ commR CommunicationRoute{..} = do
|
|||||||
((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . withButtonForm' universeF . renderAForm FormStandard $ Communication
|
((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . withButtonForm' universeF . renderAForm FormStandard $ Communication
|
||||||
<$> recipientAForm
|
<$> recipientAForm
|
||||||
<* aformMessage recipientsListMsg
|
<* aformMessage recipientsListMsg
|
||||||
<*> aopt textField (fslI MsgCommSubject & addAttr "uw-enter-as-tab" "") Nothing
|
<*> ( CommunicationContent
|
||||||
<*> (markupOutput <$> areq htmlField (fslI MsgCommBody) Nothing)
|
<$> aopt textField (fslI MsgCommSubject & addAttr "uw-enter-as-tab" "") Nothing
|
||||||
|
<*> (markupOutput <$> areq htmlField (fslI MsgCommBody) Nothing)
|
||||||
|
<*> fmap fold (aopt (convertFieldM (runConduit . (.| C.foldMap Set.singleton)) yieldMany fileFieldMultiple) (fslI MsgCommAttachments & setTooltip MsgCommAttachmentsTip) Nothing)
|
||||||
|
)
|
||||||
formResult commRes $ \case
|
formResult commRes $ \case
|
||||||
(comm, BtnCommunicationSend) -> do
|
(comm, BtnCommunicationSend) -> do
|
||||||
runDBJobs . runConduit $ transPipe (mapReaderT lift) (crJobs comm) .| sinkDBJobs
|
runDBJobs . runConduit $ transPipe (mapReaderT lift) (crJobs comm) .| sinkDBJobs
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
module Handler.Utils.Files
|
module Handler.Utils.Files
|
||||||
( sourceFile, sourceFile'
|
( sourceFile, sourceFile'
|
||||||
, sourceFiles, sourceFiles'
|
, sourceFiles, sourceFiles'
|
||||||
@ -9,6 +11,7 @@ module Handler.Utils.Files
|
|||||||
|
|
||||||
import Import.NoFoundation hiding (First(..))
|
import Import.NoFoundation hiding (First(..))
|
||||||
import Foundation.Type
|
import Foundation.Type
|
||||||
|
import Foundation.DB
|
||||||
import Utils.Metrics
|
import Utils.Metrics
|
||||||
|
|
||||||
import Data.Monoid (First(..))
|
import Data.Monoid (First(..))
|
||||||
@ -181,6 +184,11 @@ sourceFiles' = C.map sourceFile'
|
|||||||
sourceFile' :: forall file. (HasFileReference file, YesodPersistBackend UniWorX ~ SqlBackend) => file -> DBFile
|
sourceFile' :: forall file. (HasFileReference file, YesodPersistBackend UniWorX ~ SqlBackend) => file -> DBFile
|
||||||
sourceFile' = sourceFile . view (_FileReference . _1)
|
sourceFile' = sourceFile . view (_FileReference . _1)
|
||||||
|
|
||||||
|
|
||||||
|
instance (YesodMail UniWorX, YesodPersistBackend UniWorX ~ SqlBackend) => ToMailPart UniWorX FileReference where
|
||||||
|
toMailPart = toMailPart <=< liftHandler . runDBRead . withReaderT projectBackend . toPureFile . sourceFile'
|
||||||
|
|
||||||
|
|
||||||
respondFileConditional :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, YesodPersistBackend UniWorX ~ SqlBackend, YesodPersistRunner UniWorX)
|
respondFileConditional :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, YesodPersistBackend UniWorX ~ SqlBackend, YesodPersistRunner UniWorX)
|
||||||
=> Maybe UTCTime -> MimeType
|
=> Maybe UTCTime -> MimeType
|
||||||
-> FileReference
|
-> FileReference
|
||||||
|
|||||||
@ -47,6 +47,9 @@ import qualified Data.Foldable as F
|
|||||||
|
|
||||||
import qualified Control.Monad.State.Class as State
|
import qualified Control.Monad.State.Class as State
|
||||||
|
|
||||||
|
import Jobs.Types
|
||||||
|
import Data.Aeson.Lens (_JSON)
|
||||||
|
|
||||||
|
|
||||||
dispatchJobPruneSessionFiles :: JobHandler UniWorX
|
dispatchJobPruneSessionFiles :: JobHandler UniWorX
|
||||||
dispatchJobPruneSessionFiles = JobHandlerAtomicWithFinalizer act fin
|
dispatchJobPruneSessionFiles = JobHandlerAtomicWithFinalizer act fin
|
||||||
@ -83,6 +86,9 @@ workflowFileReferences = Map.fromList $ over (traverse . _1) nameToPathPiece
|
|||||||
, (''WorkflowWorkflow, E.selectSource (E.from $ pure . (E.^. WorkflowWorkflowState )) .| awaitForever (mapMOf_ (typesCustom @WorkflowChildren . _fileReferenceContent . _Just) yield . E.unValue))
|
, (''WorkflowWorkflow, E.selectSource (E.from $ pure . (E.^. WorkflowWorkflowState )) .| awaitForever (mapMOf_ (typesCustom @WorkflowChildren . _fileReferenceContent . _Just) yield . E.unValue))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
jobFileReferences :: MonadResource m => ConduitT () FileContentReference (SqlPersistT m) ()
|
||||||
|
jobFileReferences = E.selectSource (E.from $ pure . (E.^. QueuedJobContent)) .| C.mapMaybe (preview _JSON . E.unValue) .| awaitForever (mapMOf_ (typesCustom @JobChildren @Job @Job @FileContentReference @FileContentReference) yield)
|
||||||
|
|
||||||
|
|
||||||
dispatchJobDetectMissingFiles :: JobHandler UniWorX
|
dispatchJobDetectMissingFiles :: JobHandler UniWorX
|
||||||
dispatchJobDetectMissingFiles = JobHandlerAtomicDeferrableWithFinalizer act fin
|
dispatchJobDetectMissingFiles = JobHandlerAtomicDeferrableWithFinalizer act fin
|
||||||
@ -103,8 +109,10 @@ dispatchJobDetectMissingFiles = JobHandlerAtomicDeferrableWithFinalizer act fin
|
|||||||
E.distinctOnOrderBy [E.asc ref] $ return ref
|
E.distinctOnOrderBy [E.asc ref] $ return ref
|
||||||
transPipe lift (E.selectSource fileReferencesQuery) .| C.mapMaybe E.unValue .| C.mapM_ (insertRef refKind)
|
transPipe lift (E.selectSource fileReferencesQuery) .| C.mapMaybe E.unValue .| C.mapM_ (insertRef refKind)
|
||||||
|
|
||||||
iforM_ workflowFileReferences $ \refKind refSource ->
|
let useRefSource refKind refSource = transPipe (lift . withReaderT projectBackend) (refSource .| C.filterM (\ref -> not <$> exists [FileContentEntryHash ==. ref])) .| C.mapM_ (insertRef refKind)
|
||||||
transPipe (lift . withReaderT projectBackend) (refSource .| C.filterM (\ref -> not <$> exists [FileContentEntryHash ==. ref])) .| C.mapM_ (insertRef refKind)
|
iforM_ workflowFileReferences useRefSource
|
||||||
|
useRefSource (nameToPathPiece ''Job) jobFileReferences
|
||||||
|
|
||||||
|
|
||||||
let allMissingDb :: Set Minio.Object
|
let allMissingDb :: Set Minio.Object
|
||||||
allMissingDb = setOf (folded . folded . re minioFileReference) missingDb
|
allMissingDb = setOf (folded . folded . re minioFileReference) missingDb
|
||||||
@ -204,14 +212,16 @@ dispatchJobPruneUnreferencedFiles numIterations epoch iteration = JobHandlerAtom
|
|||||||
return $ E.any E.exists (fileReferences $ fileContentEntry E.^. FileContentEntryHash)
|
return $ E.any E.exists (fileReferences $ fileContentEntry E.^. FileContentEntryHash)
|
||||||
E.where_ $ chunkIdFilter unreferencedChunkHash
|
E.where_ $ chunkIdFilter unreferencedChunkHash
|
||||||
|
|
||||||
let unmarkWorkflowFiles (otoList -> fRefs) = E.delete . E.from $ \fileContentChunkUnreferenced -> do
|
let unmarkSourceFiles (otoList -> fRefs) = E.delete . E.from $ \fileContentChunkUnreferenced -> do
|
||||||
let unreferencedChunkHash = E.unKey $ fileContentChunkUnreferenced E.^. FileContentChunkUnreferencedHash
|
let unreferencedChunkHash = E.unKey $ fileContentChunkUnreferenced E.^. FileContentChunkUnreferencedHash
|
||||||
E.where_ . E.subSelectOr . E.from $ \fileContentEntry -> do
|
E.where_ . E.subSelectOr . E.from $ \fileContentEntry -> do
|
||||||
E.where_ $ fileContentEntry E.^. FileContentEntryChunkHash E.==. unreferencedChunkHash
|
E.where_ $ fileContentEntry E.^. FileContentEntryChunkHash E.==. unreferencedChunkHash
|
||||||
return $ fileContentEntry E.^. FileContentEntryHash `E.in_` E.valList fRefs
|
return $ fileContentEntry E.^. FileContentEntryHash `E.in_` E.valList fRefs
|
||||||
E.where_ $ chunkIdFilter unreferencedChunkHash
|
E.where_ $ chunkIdFilter unreferencedChunkHash
|
||||||
|
unmarkRefSource refSource = runConduit $ refSource .| C.map Seq.singleton .| C.chunksOfE chunkSize .| C.mapM_ unmarkSourceFiles
|
||||||
chunkSize = 100
|
chunkSize = 100
|
||||||
in runConduit $ sequence_ workflowFileReferences .| C.map Seq.singleton .| C.chunksOfE chunkSize .| C.mapM_ unmarkWorkflowFiles
|
unmarkRefSource $ sequence_ workflowFileReferences
|
||||||
|
unmarkRefSource jobFileReferences
|
||||||
|
|
||||||
let
|
let
|
||||||
getEntryCandidates = E.selectSource . E.from $ \fileContentEntry -> do
|
getEntryCandidates = E.selectSource . E.from $ \fileContentEntry -> do
|
||||||
|
|||||||
@ -20,10 +20,9 @@ dispatchJobSendCourseCommunication :: Either UserEmail UserId
|
|||||||
-> CourseId
|
-> CourseId
|
||||||
-> UserId
|
-> UserId
|
||||||
-> UUID
|
-> UUID
|
||||||
-> Maybe Text
|
-> CommunicationContent
|
||||||
-> Html
|
|
||||||
-> JobHandler UniWorX
|
-> JobHandler UniWorX
|
||||||
dispatchJobSendCourseCommunication jRecipientEmail jAllRecipientAddresses jCourse jSender jMailObjectUUID jSubject jMailContent = JobHandlerException $ do
|
dispatchJobSendCourseCommunication jRecipientEmail jAllRecipientAddresses jCourse jSender jMailObjectUUID CommunicationContent{..} = JobHandlerException $ do
|
||||||
(sender, Course{..}) <- runDB $ (,)
|
(sender, Course{..}) <- runDB $ (,)
|
||||||
<$> getJust jSender
|
<$> getJust jSender
|
||||||
<*> getJust jCourse
|
<*> getJust jCourse
|
||||||
@ -34,8 +33,9 @@ dispatchJobSendCourseCommunication jRecipientEmail jAllRecipientAddresses jCours
|
|||||||
_mailFrom .= userAddressFrom sender
|
_mailFrom .= userAddressFrom sender
|
||||||
addMailHeader "Cc" [st|#{mr MsgCommUndisclosedRecipients}:;|]
|
addMailHeader "Cc" [st|#{mr MsgCommUndisclosedRecipients}:;|]
|
||||||
addMailHeader "Auto-Submitted" "no"
|
addMailHeader "Auto-Submitted" "no"
|
||||||
setSubjectI . prependCourseTitle courseTerm courseSchool courseShorthand $ maybe (SomeMessage MsgCommCourseSubject) SomeMessage jSubject
|
setSubjectI . prependCourseTitle courseTerm courseSchool courseShorthand $ maybe (SomeMessage MsgCommCourseSubject) SomeMessage ccSubject
|
||||||
addHtmlMarkdownAlternatives ($(ihamletFile "templates/mail/courseCommunication.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
addHtmlMarkdownAlternatives ($(ihamletFile "templates/mail/courseCommunication.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
||||||
|
forM_ ccAttachments $ addPart' . toMailPart
|
||||||
when (jRecipientEmail == Right jSender) $
|
when (jRecipientEmail == Right jSender) $
|
||||||
addPart' $ do
|
addPart' $ do
|
||||||
partIsAttachmentCsv MsgCommAllRecipients
|
partIsAttachmentCsv MsgCommAllRecipients
|
||||||
|
|||||||
@ -44,7 +44,7 @@ import Cron (CronNextMatch(..), _MatchAsap, _MatchAt, _MatchNone)
|
|||||||
import System.Clock (getTime, Clock(Monotonic), TimeSpec)
|
import System.Clock (getTime, Clock(Monotonic), TimeSpec)
|
||||||
import GHC.Conc (unsafeIOToSTM)
|
import GHC.Conc (unsafeIOToSTM)
|
||||||
|
|
||||||
import Data.Generics.Product.Types (Children, ChGeneric)
|
import Data.Generics.Product.Types (Children, ChGeneric, HasTypesCustom(..))
|
||||||
|
|
||||||
{-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-}
|
{-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-}
|
||||||
|
|
||||||
@ -67,8 +67,7 @@ data Job
|
|||||||
, jCourse :: CourseId
|
, jCourse :: CourseId
|
||||||
, jSender :: UserId
|
, jSender :: UserId
|
||||||
, jMailObjectUUID :: UUID
|
, jMailObjectUUID :: UUID
|
||||||
, jSubject :: Maybe Text
|
, jMailContent :: CommunicationContent
|
||||||
, jMailContent :: Html
|
|
||||||
}
|
}
|
||||||
| JobInvitation { jInviter :: Maybe UserId
|
| JobInvitation { jInviter :: Maybe UserId
|
||||||
, jInvitee :: UserEmail
|
, jInvitee :: UserEmail
|
||||||
@ -169,10 +168,14 @@ type family ChildrenJobChildren a where
|
|||||||
ChildrenJobChildren UUID = '[]
|
ChildrenJobChildren UUID = '[]
|
||||||
ChildrenJobChildren (Key a) = '[]
|
ChildrenJobChildren (Key a) = '[]
|
||||||
ChildrenJobChildren (CI a) = '[]
|
ChildrenJobChildren (CI a) = '[]
|
||||||
ChildrenJobChildren (Set a) = '[]
|
ChildrenJobChildren (Set v) = '[v]
|
||||||
ChildrenJobChildren MailContext = '[]
|
ChildrenJobChildren MailContext = '[]
|
||||||
|
ChildrenJobChildren (Digest a) = '[]
|
||||||
|
|
||||||
ChildrenJobChildren a = Children ChGeneric a
|
ChildrenJobChildren a = Children ChGeneric a
|
||||||
|
|
||||||
|
instance (Ord b', HasTypesCustom JobChildren a' b' a b) => HasTypesCustom JobChildren (Set a') (Set b') a b where
|
||||||
|
typesCustom = iso Set.toList Set.fromList . traverse . typesCustom @JobChildren
|
||||||
|
|
||||||
|
|
||||||
classifyJob :: Job -> String
|
classifyJob :: Job -> String
|
||||||
|
|||||||
23
src/Mail.hs
23
src/Mail.hs
@ -42,6 +42,7 @@ import Data.Kind (Type)
|
|||||||
|
|
||||||
import Model.Types.Languages
|
import Model.Types.Languages
|
||||||
import Model.Types.Csv
|
import Model.Types.Csv
|
||||||
|
import Model.Types.File
|
||||||
|
|
||||||
import Network.Mail.Mime hiding (addPart, addAttachment)
|
import Network.Mail.Mime hiding (addPart, addAttachment)
|
||||||
import qualified Network.Mail.Mime as Mime (addPart)
|
import qualified Network.Mail.Mime as Mime (addPart)
|
||||||
@ -89,7 +90,7 @@ import qualified Data.Binary as Binary
|
|||||||
import "network-bsd" Network.BSD (getHostName)
|
import "network-bsd" Network.BSD (getHostName)
|
||||||
|
|
||||||
import Data.Time.Zones (utcTZ, utcToLocalTimeTZ, timeZoneForUTCTime)
|
import Data.Time.Zones (utcTZ, utcToLocalTimeTZ, timeZoneForUTCTime)
|
||||||
import Data.Time.LocalTime (ZonedTime(..), TimeZone(..))
|
import Data.Time.LocalTime (ZonedTime(..), TimeZone(..), utcToZonedTime, utc)
|
||||||
import Data.Time.Format (rfc822DateFormat)
|
import Data.Time.Format (rfc822DateFormat)
|
||||||
|
|
||||||
import Network.HaskellNet.SMTP (SMTPConnection)
|
import Network.HaskellNet.SMTP (SMTPConnection)
|
||||||
@ -123,6 +124,12 @@ import Language.Haskell.TH (nameBase)
|
|||||||
|
|
||||||
import Network.Mail.Mime.Instances()
|
import Network.Mail.Mime.Instances()
|
||||||
|
|
||||||
|
import Data.Char (isLatin1)
|
||||||
|
import Data.Text.Lazy.Encoding (decodeUtf8')
|
||||||
|
import System.FilePath (takeFileName)
|
||||||
|
import Network.HTTP.Types.Header (hETag)
|
||||||
|
import Web.HttpApiData (ToHttpApiData(toHeader))
|
||||||
|
|
||||||
|
|
||||||
makeLenses_ ''Address
|
makeLenses_ ''Address
|
||||||
makeLenses_ ''Mail
|
makeLenses_ ''Mail
|
||||||
@ -346,6 +353,20 @@ instance YesodMail site => ToMailPart site Html where
|
|||||||
_partEncoding .= QuotedPrintableText
|
_partEncoding .= QuotedPrintableText
|
||||||
_partContent .= PartContent (renderMarkup html)
|
_partContent .= PartContent (renderMarkup html)
|
||||||
|
|
||||||
|
instance YesodMail site => ToMailPart site PureFile where
|
||||||
|
toMailPart file@File{fileTitle, fileModified} = do
|
||||||
|
_partDisposition .= AttachmentDisposition (pack $ takeFileName fileTitle)
|
||||||
|
_partType .= decodeUtf8 (mimeLookup $ pack fileTitle)
|
||||||
|
let
|
||||||
|
content :: LBS.ByteString
|
||||||
|
content = file ^. _pureFileContent . _Just
|
||||||
|
isLatin = either (const False) (all isLatin1) $ decodeUtf8' content
|
||||||
|
_partEncoding .= bool Base64 QuotedPrintableText isLatin
|
||||||
|
_partContent .= PartContent content
|
||||||
|
forM_ (file ^. _FileReference . _1 . _fileReferenceContent) $ \fRefContent ->
|
||||||
|
replaceMailHeader (CI.original hETag) . Just . decodeUtf8 . toHeader $ etagFileReference # fRefContent
|
||||||
|
replaceMailHeader (CI.original hLastModified) . Just . pack . formatTime defaultTimeLocale rfc822DateFormat $ utcToZonedTime utc fileModified
|
||||||
|
|
||||||
instance (ToMailPart site a, RenderMessage site msg) => ToMailPart site (Hamlet.Translate msg -> a) where
|
instance (ToMailPart site a, RenderMessage site msg) => ToMailPart site (Hamlet.Translate msg -> a) where
|
||||||
type MailPartReturn site (Hamlet.Translate msg -> a) = MailPartReturn site a
|
type MailPartReturn site (Hamlet.Translate msg -> a) = MailPartReturn site a
|
||||||
toMailPart act = do
|
toMailPart act = do
|
||||||
|
|||||||
@ -23,3 +23,4 @@ import Model.Types.Markup as Types
|
|||||||
import Model.Types.Room as Types
|
import Model.Types.Room as Types
|
||||||
import Model.Types.Csv as Types
|
import Model.Types.Csv as Types
|
||||||
import Model.Types.Upload as Types
|
import Model.Types.Upload as Types
|
||||||
|
import Model.Types.Communication as Types
|
||||||
|
|||||||
21
src/Model/Types/Communication.hs
Normal file
21
src/Model/Types/Communication.hs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
module Model.Types.Communication
|
||||||
|
( CommunicationContent(..), _ccSubject, _ccBody, _ccAttachments
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import.NoModel
|
||||||
|
import Model.Types.File
|
||||||
|
|
||||||
|
import Utils.Lens.TH
|
||||||
|
|
||||||
|
|
||||||
|
data CommunicationContent = CommunicationContent
|
||||||
|
{ ccSubject :: Maybe Text
|
||||||
|
, ccBody :: Html
|
||||||
|
, ccAttachments :: Set FileReference
|
||||||
|
} deriving stock (Eq, Ord, Show, Read, Generic, Typeable)
|
||||||
|
deriving anyclass (Hashable, NFData)
|
||||||
|
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ constructorTagModifier = camelToPathPiece' 1
|
||||||
|
} ''CommunicationContent
|
||||||
|
makeLenses_ ''CommunicationContent
|
||||||
@ -18,7 +18,24 @@ module Model.Types.File
|
|||||||
, _fieldIdent, _fieldUnpackZips, _fieldMultiple, _fieldRestrictExtensions, _fieldAdditionalFiles, _fieldMaxFileSize
|
, _fieldIdent, _fieldUnpackZips, _fieldMultiple, _fieldRestrictExtensions, _fieldAdditionalFiles, _fieldMaxFileSize
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoModel
|
import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender, derivePersistFieldJSON, Proxy(..))
|
||||||
|
import Crypto.Hash (Digest, SHA3_512)
|
||||||
|
import Language.Haskell.TH.Syntax (Lift)
|
||||||
|
import Data.Binary (Binary)
|
||||||
|
import Crypto.Hash.Instances ()
|
||||||
|
import Data.Proxy (Proxy(..))
|
||||||
|
import Control.Lens
|
||||||
|
import Utils.HttpConditional
|
||||||
|
import Data.Binary.Instances.Time ()
|
||||||
|
import Data.Time.Clock.Instances ()
|
||||||
|
import Data.Aeson.TH
|
||||||
|
import Utils
|
||||||
|
import Data.Kind (Type)
|
||||||
|
import Data.Universe
|
||||||
|
import Numeric.Natural
|
||||||
|
import Network.Mime
|
||||||
|
import Control.Monad.Morph
|
||||||
|
import Data.NonNull.Instances ()
|
||||||
|
|
||||||
import Database.Persist.Sql (PersistFieldSql(..))
|
import Database.Persist.Sql (PersistFieldSql(..))
|
||||||
import Web.HttpApiData (ToHttpApiData, FromHttpApiData)
|
import Web.HttpApiData (ToHttpApiData, FromHttpApiData)
|
||||||
@ -204,7 +221,6 @@ instance HasFileReference FileReference where
|
|||||||
instance HasFileReference PureFile where
|
instance HasFileReference PureFile where
|
||||||
newtype FileReferenceResidual PureFile = PureFileResidual { unPureFileResidual :: Maybe ByteString }
|
newtype FileReferenceResidual PureFile = PureFileResidual { unPureFileResidual :: Maybe ByteString }
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
deriving newtype (ToJSON, FromJSON)
|
|
||||||
deriving anyclass (NFData)
|
deriving anyclass (NFData)
|
||||||
|
|
||||||
_FileReference = iso toFileReference fromFileReference
|
_FileReference = iso toFileReference fromFileReference
|
||||||
|
|||||||
@ -4,4 +4,4 @@ $newline never
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<body>
|
<body>
|
||||||
#{jMailContent}
|
#{ccBody}
|
||||||
|
|||||||
Reference in New Issue
Block a user