Liitä mediatiedostot viesteihin
HTML-sähköpostit voivat linkata liitteisiin, jolloin viesti on itsenäinen. Viestin sisäisiin kuviin linkkaaminen toimii yleensä sähköpostiohjelmissa ulkoisia linkkejä paremmin, koska ulkoisten linkkien seuraaminen on yksityisyysriski.
This commit is contained in:
		
							parent
							
								
									373e34a9e4
								
							
						
					
					
						commit
						eddbceba67
					
				@ -26,6 +26,8 @@
 | 
				
			|||||||
                        #:select? vcs-file?))
 | 
					                        #:select? vcs-file?))
 | 
				
			||||||
    (build-system haskell-build-system)
 | 
					    (build-system haskell-build-system)
 | 
				
			||||||
    (inputs (list ghc-acid-state
 | 
					    (inputs (list ghc-acid-state
 | 
				
			||||||
 | 
					                  ghc-attoparsec
 | 
				
			||||||
 | 
					                  ghc-base64
 | 
				
			||||||
                  ghc-cryptonite
 | 
					                  ghc-cryptonite
 | 
				
			||||||
                  ghc-case-insensitive
 | 
					                  ghc-case-insensitive
 | 
				
			||||||
                  ghc-glob
 | 
					                  ghc-glob
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 | 
					{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 | 
				
			||||||
{-# LANGUAGE NamedFieldPuns #-}
 | 
					{-# LANGUAGE NamedFieldPuns #-}
 | 
				
			||||||
{-# LANGUAGE OverloadedStrings #-}
 | 
					{-# LANGUAGE OverloadedStrings #-}
 | 
				
			||||||
 | 
					{-# LANGUAGE PackageImports #-}
 | 
				
			||||||
{-# LANGUAGE RecordWildCards #-}
 | 
					{-# LANGUAGE RecordWildCards #-}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module TiedoteMD.Read where
 | 
					module TiedoteMD.Read where
 | 
				
			||||||
@ -16,10 +17,13 @@ import Data.Acid (AcidState, update)
 | 
				
			|||||||
import Data.Bifunctor (first, second)
 | 
					import Data.Bifunctor (first, second)
 | 
				
			||||||
import Data.ByteArray (convert)
 | 
					import Data.ByteArray (convert)
 | 
				
			||||||
import Data.ByteString (ByteString)
 | 
					import Data.ByteString (ByteString)
 | 
				
			||||||
 | 
					import "base64" Data.ByteString.Base64.URL
 | 
				
			||||||
import Data.Default (def)
 | 
					import Data.Default (def)
 | 
				
			||||||
import Data.Either (rights, lefts)
 | 
					import Data.Either (rights, lefts, fromRight)
 | 
				
			||||||
import Data.FileStore (FileStore(..), Revision(..), FileStoreError(..), gitFileStore)
 | 
					import Data.FileStore (FileStore(..), Revision(..), FileStoreError(..), gitFileStore)
 | 
				
			||||||
import Data.List (singleton, isSuffixOf)
 | 
					import Data.List (singleton, isSuffixOf)
 | 
				
			||||||
 | 
					import Data.MIME (ContentID, makeContentID, renderContentID)
 | 
				
			||||||
 | 
					import Data.Maybe (fromMaybe)
 | 
				
			||||||
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
 | 
					import Data.Text.Encoding (decodeUtf8, encodeUtf8)
 | 
				
			||||||
import Data.Time (UTCTime, zonedTimeToUTC)
 | 
					import Data.Time (UTCTime, zonedTimeToUTC)
 | 
				
			||||||
import Data.Time (getCurrentTime)
 | 
					import Data.Time (getCurrentTime)
 | 
				
			||||||
@ -29,12 +33,15 @@ import System.FilePath.Glob (match, compile)
 | 
				
			|||||||
import System.IO (hClose)
 | 
					import System.IO (hClose)
 | 
				
			||||||
import System.Process (createProcess, proc, waitForProcess, CreateProcess(..), StdStream(..))
 | 
					import System.Process (createProcess, proc, waitForProcess, CreateProcess(..), StdStream(..))
 | 
				
			||||||
import Text.Pandoc (Pandoc(..), PandocMonad, PandocIO, PandocError)
 | 
					import Text.Pandoc (Pandoc(..), PandocMonad, PandocIO, PandocError)
 | 
				
			||||||
 | 
					import Text.Pandoc.MediaBag (MediaItem(..), lookupMedia, mediaItems)
 | 
				
			||||||
import Text.Pandoc.Readers (readMarkdown)
 | 
					import Text.Pandoc.Readers (readMarkdown)
 | 
				
			||||||
 | 
					import Text.Pandoc.Walk (walkM)
 | 
				
			||||||
import Text.Pandoc.Writers (writePlain, writeMarkdown, writeHtml5String)
 | 
					import Text.Pandoc.Writers (writePlain, writeMarkdown, writeHtml5String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import qualified Data.ByteString as BS
 | 
					import qualified Data.ByteString as BS
 | 
				
			||||||
import qualified Data.ByteString.Lazy as LBS
 | 
					import qualified Data.ByteString.Lazy as LBS
 | 
				
			||||||
import qualified Data.Map as Map
 | 
					import qualified Data.Map as Map
 | 
				
			||||||
 | 
					import qualified Data.Set as Set
 | 
				
			||||||
import qualified Data.Text as T
 | 
					import qualified Data.Text as T
 | 
				
			||||||
import qualified Data.Text.IO as T
 | 
					import qualified Data.Text.IO as T
 | 
				
			||||||
import qualified Text.Pandoc as Pandoc
 | 
					import qualified Text.Pandoc as Pandoc
 | 
				
			||||||
@ -101,10 +108,40 @@ readMessageFile store = flip (retrieve store) Nothing >=>
 | 
				
			|||||||
    uncurry (parseMessageFile store) . addHash . T.filter (/= '\r') . decodeUtf8 . BS.toStrict
 | 
					    uncurry (parseMessageFile store) . addHash . T.filter (/= '\r') . decodeUtf8 . BS.toStrict
 | 
				
			||||||
        where addHash text = (convert $ hashWith SHA256 $ encodeUtf8 text, text)
 | 
					        where addHash text = (convert $ hashWith SHA256 $ encodeUtf8 text, text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					cidOf :: MediaItem -> ContentID
 | 
				
			||||||
 | 
					cidOf MediaItem {mediaContents, mediaMimeType} =
 | 
				
			||||||
 | 
					    fromRight (error "makeContentID failed with valid input!") $
 | 
				
			||||||
 | 
					        makeContentID $ "<" <> encodedHash <> "@tiedote.md.sha256>"
 | 
				
			||||||
 | 
					            where encodedHash = encodeBase64' $ convert $ hashWith SHA256 $
 | 
				
			||||||
 | 
					                      LBS.toStrict mediaContents <> encodeUtf8 mediaMimeType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					cidUrl :: ContentID -> T.Text
 | 
				
			||||||
 | 
					cidUrl = mappend "cid:" . stripAngleBrackets . decodeUtf8 . renderContentID
 | 
				
			||||||
 | 
					    where stripAngleBrackets = T.init . T.tail
 | 
				
			||||||
 | 
					          -- The string is always surrounded by < > so this is safe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					makeMediaPart :: MediaItem -> MediaPart
 | 
				
			||||||
 | 
					makeMediaPart mediaItem@MediaItem {..} = MediaPart
 | 
				
			||||||
 | 
					    { mediaPartMimeType = encodeUtf8 mediaMimeType
 | 
				
			||||||
 | 
					    , mediaPartContentID = renderContentID $ cidOf mediaItem
 | 
				
			||||||
 | 
					    , mediaPartContents = LBS.toStrict mediaContents
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					reconstructMediaItem :: (FilePath, T.Text, LBS.ByteString) -> MediaItem
 | 
				
			||||||
 | 
					reconstructMediaItem (mediaPath, mediaMimeType, mediaContents) = MediaItem {..}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					replaceImagesWithCid :: PandocMonad m => Pandoc -> m Pandoc
 | 
				
			||||||
 | 
					replaceImagesWithCid = Pandoc.fillMediaBag >=> walkM handleImage
 | 
				
			||||||
 | 
					    where handleImage (Pandoc.Image attr lab (src, title)) = do
 | 
				
			||||||
 | 
					              mediaItem <- (\media -> fromMaybe (error $ "fillMediaBag left an image uncollected! impossible!")
 | 
				
			||||||
 | 
					                           $ lookupMedia (T.unpack src) media) <$> Pandoc.getMediaBag
 | 
				
			||||||
 | 
					              pure $ Pandoc.Image attr lab (cidUrl $ cidOf mediaItem, title)
 | 
				
			||||||
 | 
					          handleImage x = pure x
 | 
				
			||||||
 | 
					
 | 
				
			||||||
parseMessageFile :: FileStore -> ByteString -> T.Text -> IO (Either Error Message)
 | 
					parseMessageFile :: FileStore -> ByteString -> T.Text -> IO (Either Error Message)
 | 
				
			||||||
parseMessageFile store hash text = fmap (join . first PandocError) . runReadM store $ do
 | 
					parseMessageFile store hash text = fmap (join . first PandocError) . runReadM store $ do
 | 
				
			||||||
    pandoc@(Pandoc meta _) <- flip readMarkdown text
 | 
					    pandoc@(Pandoc meta _) <- flip readMarkdown text
 | 
				
			||||||
        def {Pandoc.readerStandalone = True, Pandoc.readerExtensions = Pandoc.pandocExtensions}
 | 
					        def {Pandoc.readerStandalone = True, Pandoc.readerExtensions = Pandoc.pandocExtensions} >>= replaceImagesWithCid
 | 
				
			||||||
    let tiedoteMeta = do
 | 
					    let tiedoteMeta = do
 | 
				
			||||||
            previewTo <- lookupMeta' "tarkistaja" meta >>= metaToEmails
 | 
					            previewTo <- lookupMeta' "tarkistaja" meta >>= metaToEmails
 | 
				
			||||||
            previewTime <- lookupMeta' "deadline" meta >>= metaToTime
 | 
					            previewTime <- lookupMeta' "deadline" meta >>= metaToTime
 | 
				
			||||||
@ -120,6 +157,7 @@ parseMessageFile store hash text = fmap (join . first PandocError) . runReadM st
 | 
				
			|||||||
                  (Pandoc.Meta $ Map.insertWith (flip const) "pagetitle" (Pandoc.MetaString subject) $ Pandoc.unMeta meta')
 | 
					                  (Pandoc.Meta $ Map.insertWith (flip const) "pagetitle" (Pandoc.MetaString subject) $ Pandoc.unMeta meta')
 | 
				
			||||||
                  blocks'
 | 
					                  blocks'
 | 
				
			||||||
          htmlMessage <- liftIO . inlineCSS =<< renderHelper writeHtml5String htmlTemplate htmlPandoc
 | 
					          htmlMessage <- liftIO . inlineCSS =<< renderHelper writeHtml5String htmlTemplate htmlPandoc
 | 
				
			||||||
 | 
					          mediaParts <- Set.fromList . map (makeMediaPart . reconstructMediaItem) . mediaItems <$> Pandoc.getMediaBag
 | 
				
			||||||
          pure $ pure $ Message
 | 
					          pure $ pure $ Message
 | 
				
			||||||
            { recipients = []
 | 
					            { recipients = []
 | 
				
			||||||
            , messageHash = hash
 | 
					            , messageHash = hash
 | 
				
			||||||
@ -133,7 +171,6 @@ parseMessageFile store hash text = fmap (join . first PandocError) . runReadM st
 | 
				
			|||||||
                  , Pandoc.writerTableOfContents = True
 | 
					                  , Pandoc.writerTableOfContents = True
 | 
				
			||||||
                  , Pandoc.writerSectionDivs = True
 | 
					                  , Pandoc.writerSectionDivs = True
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
    -- TODO: Store the media somewhere
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
inlineCSS :: T.Text -> IO T.Text
 | 
					inlineCSS :: T.Text -> IO T.Text
 | 
				
			||||||
inlineCSS html = do
 | 
					inlineCSS html = do
 | 
				
			||||||
 | 
				
			|||||||
@ -54,7 +54,8 @@ managePreviews acid sender sendmailPath = forever $ do
 | 
				
			|||||||
      Just msg@(Message {..}) -> do
 | 
					      Just msg@(Message {..}) -> do
 | 
				
			||||||
          mailID <- uniqueMailID sender
 | 
					          mailID <- uniqueMailID sender
 | 
				
			||||||
          boundary <- getStdRandom uniform
 | 
					          boundary <- getStdRandom uniform
 | 
				
			||||||
          let mail = renderMessage' "Esikatselu: " (Just mailID) msg sender boundary
 | 
					          boundary' <- getStdRandom uniform
 | 
				
			||||||
 | 
					          let mail = renderMessage' "Esikatselu: " (Just mailID) msg sender boundary boundary'
 | 
				
			||||||
          sendmail sendmailPath $ toLazyByteString $ buildMessage $
 | 
					          sendmail sendmailPath $ toLazyByteString $ buildMessage $
 | 
				
			||||||
              set (headerTo defaultCharsets) (map (Single . emailToMailbox) previewTo) mail
 | 
					              set (headerTo defaultCharsets) (map (Single . emailToMailbox) previewTo) mail
 | 
				
			||||||
          update acid $ SetPreviewID messageHash mailID
 | 
					          update acid $ SetPreviewID messageHash mailID
 | 
				
			||||||
 | 
				
			|||||||
@ -5,12 +5,14 @@ module TiedoteMD.Send where
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import Control.Concurrent (threadDelay)
 | 
					import Control.Concurrent (threadDelay)
 | 
				
			||||||
import Control.Exception (throwIO)
 | 
					import Control.Exception (throwIO)
 | 
				
			||||||
import Control.Lens (set)
 | 
					import Control.Lens (set, _Just)
 | 
				
			||||||
import Control.Monad (forever, unless)
 | 
					import Control.Monad (forever, unless)
 | 
				
			||||||
import Data.Acid (AcidState, query, update)
 | 
					import Data.Acid (AcidState, query, update)
 | 
				
			||||||
 | 
					import Data.Attoparsec.ByteString (endOfInput, parseOnly)
 | 
				
			||||||
import Data.Binary.Builder (toLazyByteString)
 | 
					import Data.Binary.Builder (toLazyByteString)
 | 
				
			||||||
import Data.MIME (Address(..), Mailbox, Boundary, MIMEMessage, MIME(..), Headers(..), MultipartSubtype(..), buildMessage, headerTo, headerSubject, headerFrom, headerMessageID, header, createTextPlainMessage, contentType)
 | 
					import Data.MIME (Address(..), Mailbox, Boundary, MIMEMessage, MIME(..), Headers(..), MultipartSubtype(..), ContentTypeWith(..), DispositionType(..), buildMessage, headerTo, headerSubject, headerFrom, headerMessageID, headerContentID, header, createTextPlainMessage, contentType, contentDisposition, dispositionType, createAttachment, parseContentType, makeContentID)
 | 
				
			||||||
import Data.MIME.Charset (defaultCharsets)
 | 
					import Data.MIME.Charset (defaultCharsets)
 | 
				
			||||||
 | 
					import Data.Set (Set)
 | 
				
			||||||
import Data.Time (getCurrentTime)
 | 
					import Data.Time (getCurrentTime)
 | 
				
			||||||
import System.Exit (ExitCode(..))
 | 
					import System.Exit (ExitCode(..))
 | 
				
			||||||
import System.Exit.Codes (codeTempFail)
 | 
					import System.Exit.Codes (codeTempFail)
 | 
				
			||||||
@ -21,6 +23,7 @@ import System.Random (getStdRandom, uniform)
 | 
				
			|||||||
import qualified Data.ByteString.Lazy as LBS
 | 
					import qualified Data.ByteString.Lazy as LBS
 | 
				
			||||||
import qualified Data.IMF as IMF
 | 
					import qualified Data.IMF as IMF
 | 
				
			||||||
import qualified Data.List.NonEmpty as NE
 | 
					import qualified Data.List.NonEmpty as NE
 | 
				
			||||||
 | 
					import qualified Data.Set as Set
 | 
				
			||||||
import qualified Data.Text as T
 | 
					import qualified Data.Text as T
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import TiedoteMD.State
 | 
					import TiedoteMD.State
 | 
				
			||||||
@ -42,7 +45,8 @@ manageQueue acid sender sendmailPath = forever $ do
 | 
				
			|||||||
      Nothing -> pure ()
 | 
					      Nothing -> pure ()
 | 
				
			||||||
      Just (address, message) -> do
 | 
					      Just (address, message) -> do
 | 
				
			||||||
          boundary <- getStdRandom uniform
 | 
					          boundary <- getStdRandom uniform
 | 
				
			||||||
          let mail = renderMessage message sender boundary
 | 
					          boundary' <- getStdRandom uniform
 | 
				
			||||||
 | 
					          let mail = renderMessage message sender boundary boundary'
 | 
				
			||||||
          sendmail sendmailPath $ toLazyByteString $ buildMessage $
 | 
					          sendmail sendmailPath $ toLazyByteString $ buildMessage $
 | 
				
			||||||
              set (headerTo defaultCharsets) [Single $ emailToMailbox address] mail
 | 
					              set (headerTo defaultCharsets) [Single $ emailToMailbox address] mail
 | 
				
			||||||
          update acid MarkMessageAsSent
 | 
					          update acid MarkMessageAsSent
 | 
				
			||||||
@ -58,11 +62,11 @@ manageQueueingMessages acid = forever $ do
 | 
				
			|||||||
queueMessages :: AcidState State -> IO ()
 | 
					queueMessages :: AcidState State -> IO ()
 | 
				
			||||||
queueMessages acid = getCurrentTime >>= update acid . MoveToSendQueue
 | 
					queueMessages acid = getCurrentTime >>= update acid . MoveToSendQueue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
renderMessage :: Message -> Mailbox -> Boundary -> MIMEMessage
 | 
					renderMessage :: Message -> Mailbox -> Boundary -> Boundary -> MIMEMessage
 | 
				
			||||||
renderMessage = renderMessage' "" Nothing
 | 
					renderMessage = renderMessage' "" Nothing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
renderMessage' :: T.Text -> Maybe MailID -> Message -> Mailbox -> Boundary -> MIMEMessage
 | 
					renderMessage' :: T.Text -> Maybe MailID -> Message -> Mailbox -> Boundary -> Boundary -> MIMEMessage
 | 
				
			||||||
renderMessage' subjectPrefix maybeMailID (Message {messageContent = MessageContent {..},..}) sender boundary =
 | 
					renderMessage' subjectPrefix maybeMailID (Message {messageContent = MessageContent {..},..}) sender boundary boundary' =
 | 
				
			||||||
    maybe id (set headerMessageID . Just . mailIDToMessageID) maybeMailID $
 | 
					    maybe id (set headerMessageID . Just . mailIDToMessageID) maybeMailID $
 | 
				
			||||||
    set (headerSubject defaultCharsets) (Just $ subjectPrefix <> subject) $
 | 
					    set (headerSubject defaultCharsets) (Just $ subjectPrefix <> subject) $
 | 
				
			||||||
    set (header "Precedence") "Bulk" $
 | 
					    set (header "Precedence") "Bulk" $
 | 
				
			||||||
@ -71,12 +75,25 @@ renderMessage' subjectPrefix maybeMailID (Message {messageContent = MessageConte
 | 
				
			|||||||
    IMF.Message (Headers []) $ Multipart Alternative boundary $ NE.fromList
 | 
					    IMF.Message (Headers []) $ Multipart Alternative boundary $ NE.fromList
 | 
				
			||||||
        [ createTextPlainMessage plainTextMessage
 | 
					        [ createTextPlainMessage plainTextMessage
 | 
				
			||||||
        , createTextMarkdownMessage markdownMessage
 | 
					        , createTextMarkdownMessage markdownMessage
 | 
				
			||||||
        , createTextHtmlMessage htmlMessage
 | 
					        , createTextHtmlMessage boundary' mediaParts htmlMessage
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
createTextMarkdownMessage :: T.Text -> MIMEMessage
 | 
					createTextMarkdownMessage :: T.Text -> MIMEMessage
 | 
				
			||||||
createTextMarkdownMessage = set contentType "text/markdown; charset=utf-8; variant=pandoc" . createTextPlainMessage
 | 
					createTextMarkdownMessage = set contentType "text/markdown; charset=utf-8; variant=pandoc" . createTextPlainMessage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
createTextHtmlMessage :: T.Text -> MIMEMessage
 | 
					createTextHtmlMessage :: Boundary -> Set MediaPart -> T.Text -> MIMEMessage
 | 
				
			||||||
createTextHtmlMessage = set contentType "text/html; charset=utf-8" . createTextPlainMessage
 | 
					createTextHtmlMessage boundary mediaParts html = IMF.Message (Headers []) $ multipartRelated $
 | 
				
			||||||
-- TODO: multipart/related with media
 | 
					    (set contentType "text/html; charset=utf-8" $ createTextPlainMessage html) NE.:| mediaAttachments
 | 
				
			||||||
 | 
					        where multipartRelated = Multipart
 | 
				
			||||||
 | 
					                  (Related (Just $ ContentType "text" "html" ()) Nothing Nothing) boundary
 | 
				
			||||||
 | 
					              mediaAttachments = mediaPartToAttachment <$> Set.toList mediaParts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mediaPartToAttachment :: MediaPart -> MIMEMessage
 | 
				
			||||||
 | 
					mediaPartToAttachment MediaPart {..} =
 | 
				
			||||||
 | 
					    set (contentDisposition . _Just . dispositionType) Inline $
 | 
				
			||||||
 | 
					    set headerContentID (Just contentID) $
 | 
				
			||||||
 | 
					    createAttachment mimeType Nothing mediaPartContents
 | 
				
			||||||
 | 
					        where mimeType = either (error "purebred-email couldn't parse pandoc's mime type!") id $
 | 
				
			||||||
 | 
					                  parseOnly (parseContentType <* endOfInput) mediaPartMimeType
 | 
				
			||||||
 | 
					              contentID = either (error "purebred-email couldn't parse it's own contentID!") id $
 | 
				
			||||||
 | 
					                  makeContentID mediaPartContentID
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,14 @@
 | 
				
			|||||||
{-# LANGUAGE OverloadedStrings #-}
 | 
					{-# LANGUAGE OverloadedStrings #-}
 | 
				
			||||||
{-# LANGUAGE RecordWildCards #-}
 | 
					{-# LANGUAGE RecordWildCards #-}
 | 
				
			||||||
{-# LANGUAGE TemplateHaskell #-}
 | 
					{-# LANGUAGE TemplateHaskell #-}
 | 
				
			||||||
 | 
					{-# LANGUAGE DuplicateRecordFields #-}
 | 
				
			||||||
 | 
					{-# LANGUAGE TypeFamilies #-}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module TiedoteMD.Types
 | 
					module TiedoteMD.Types
 | 
				
			||||||
    ( Email(..)
 | 
					    ( Email(..)
 | 
				
			||||||
    , Error(..)
 | 
					    , Error(..)
 | 
				
			||||||
    , MailID
 | 
					    , MailID
 | 
				
			||||||
 | 
					    , MediaPart(..)
 | 
				
			||||||
    , Message(..)
 | 
					    , Message(..)
 | 
				
			||||||
    , MessageContent(..)
 | 
					    , MessageContent(..)
 | 
				
			||||||
    , SendJob(..)
 | 
					    , SendJob(..)
 | 
				
			||||||
@ -28,8 +31,9 @@ import Data.CaseInsensitive (original, mk)
 | 
				
			|||||||
import Data.IMF (MessageID, Mailbox(..), AddrSpec(..), Domain(..), mailbox, parseMessageID, parse, renderMessageID, renderMailbox)
 | 
					import Data.IMF (MessageID, Mailbox(..), AddrSpec(..), Domain(..), mailbox, parseMessageID, parse, renderMessageID, renderMailbox)
 | 
				
			||||||
import Data.List.NonEmpty (NonEmpty)
 | 
					import Data.List.NonEmpty (NonEmpty)
 | 
				
			||||||
import Data.MIME.Charset (defaultCharsets)
 | 
					import Data.MIME.Charset (defaultCharsets)
 | 
				
			||||||
import Data.SafeCopy (base, deriveSafeCopy)
 | 
					import Data.SafeCopy (Migrate(..), base, deriveSafeCopy, extension)
 | 
				
			||||||
import Data.Semigroup (sconcat)
 | 
					import Data.Semigroup (sconcat)
 | 
				
			||||||
 | 
					import Data.Set (Set)
 | 
				
			||||||
import Data.Text (Text)
 | 
					import Data.Text (Text)
 | 
				
			||||||
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
 | 
					import Data.Text.Encoding (encodeUtf8, decodeUtf8)
 | 
				
			||||||
import Data.Time (UTCTime, getCurrentTime, defaultTimeLocale, formatTime)
 | 
					import Data.Time (UTCTime, getCurrentTime, defaultTimeLocale, formatTime)
 | 
				
			||||||
@ -88,6 +92,23 @@ data Message = Message
 | 
				
			|||||||
    } deriving (Show, Eq, Typeable)
 | 
					    } deriving (Show, Eq, Typeable)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
data MessageContent = MessageContent
 | 
					data MessageContent = MessageContent
 | 
				
			||||||
 | 
					    { plainTextMessage :: Text
 | 
				
			||||||
 | 
					    , markdownMessage :: Text
 | 
				
			||||||
 | 
					    , htmlMessage :: Text
 | 
				
			||||||
 | 
					    , mediaParts :: Set MediaPart
 | 
				
			||||||
 | 
					    } deriving (Show, Eq, Typeable)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data MediaPart = MediaPart
 | 
				
			||||||
 | 
					  -- XXX Ideally purebred-email would implement safecopy and we could use proper types
 | 
				
			||||||
 | 
					    { mediaPartMimeType :: ByteString -- ContentTypeWith Parameters
 | 
				
			||||||
 | 
					    , mediaPartContentID :: ByteString -- ContentID
 | 
				
			||||||
 | 
					    , mediaPartContents :: ByteString
 | 
				
			||||||
 | 
					    } deriving (Show, Typeable, Ord)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					instance Eq MediaPart where
 | 
				
			||||||
 | 
					    MediaPart {mediaPartContentID = a} == MediaPart {mediaPartContentID = b} = a == b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data MessageContent_v0 = MessageContent_v0
 | 
				
			||||||
    { plainTextMessage :: Text
 | 
					    { plainTextMessage :: Text
 | 
				
			||||||
    , markdownMessage :: Text
 | 
					    , markdownMessage :: Text
 | 
				
			||||||
    , htmlMessage :: Text
 | 
					    , htmlMessage :: Text
 | 
				
			||||||
@ -148,7 +169,14 @@ renderError (FileNotFoundError path) = T.pack path <> " not found"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
deriveSafeCopy 0 'base ''Domain'
 | 
					deriveSafeCopy 0 'base ''Domain'
 | 
				
			||||||
deriveSafeCopy 0 'base ''Email
 | 
					deriveSafeCopy 0 'base ''Email
 | 
				
			||||||
deriveSafeCopy 0 'base ''MessageContent
 | 
					deriveSafeCopy 0 'base ''MessageContent_v0
 | 
				
			||||||
 | 
					deriveSafeCopy 0 'base ''MediaPart
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					instance Migrate MessageContent where
 | 
				
			||||||
 | 
					    type MigrateFrom MessageContent = MessageContent_v0
 | 
				
			||||||
 | 
					    migrate MessageContent_v0 {..} = MessageContent {mediaParts = mempty, ..}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					deriveSafeCopy 1 'extension ''MessageContent
 | 
				
			||||||
deriveSafeCopy 0 'base ''MailID
 | 
					deriveSafeCopy 0 'base ''MailID
 | 
				
			||||||
deriveSafeCopy 0 'base ''Message
 | 
					deriveSafeCopy 0 'base ''Message
 | 
				
			||||||
deriveSafeCopy 0 'base ''SendJob
 | 
					deriveSafeCopy 0 'base ''SendJob
 | 
				
			||||||
 | 
				
			|||||||
@ -23,7 +23,9 @@ source-repository head
 | 
				
			|||||||
executable tiedote.md
 | 
					executable tiedote.md
 | 
				
			||||||
  build-depends:
 | 
					  build-depends:
 | 
				
			||||||
    acid-state,
 | 
					    acid-state,
 | 
				
			||||||
 | 
					    attoparsec,
 | 
				
			||||||
    base,
 | 
					    base,
 | 
				
			||||||
 | 
					    base64,
 | 
				
			||||||
    binary,
 | 
					    binary,
 | 
				
			||||||
    bytestring,
 | 
					    bytestring,
 | 
				
			||||||
    case-insensitive,
 | 
					    case-insensitive,
 | 
				
			||||||
@ -35,6 +37,7 @@ executable tiedote.md
 | 
				
			|||||||
    doctemplates,
 | 
					    doctemplates,
 | 
				
			||||||
    exit-codes,
 | 
					    exit-codes,
 | 
				
			||||||
    file-embed,
 | 
					    file-embed,
 | 
				
			||||||
 | 
					    filepath,
 | 
				
			||||||
    filestore,
 | 
					    filestore,
 | 
				
			||||||
    Glob,
 | 
					    Glob,
 | 
				
			||||||
    hostname,
 | 
					    hostname,
 | 
				
			||||||
@ -44,6 +47,7 @@ executable tiedote.md
 | 
				
			|||||||
    network,
 | 
					    network,
 | 
				
			||||||
    optparse-applicative,
 | 
					    optparse-applicative,
 | 
				
			||||||
    pandoc,
 | 
					    pandoc,
 | 
				
			||||||
 | 
					    pandoc-types,
 | 
				
			||||||
    process,
 | 
					    process,
 | 
				
			||||||
    purebred-email,
 | 
					    purebred-email,
 | 
				
			||||||
    random,
 | 
					    random,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user