diff options
-rw-r--r-- | lib/Postdelay/Queue.hs | 108 | ||||
-rw-r--r-- | lib/Postdelay/Scan.hs | 1 | ||||
-rw-r--r-- | lib/Postdelay/TimeSpec.hs | 2 | ||||
-rw-r--r-- | postdelay.cabal | 15 | ||||
-rw-r--r-- | postdelay.nix | 21 | ||||
-rw-r--r-- | src/Simple.hs | 77 |
6 files changed, 195 insertions, 29 deletions
diff --git a/lib/Postdelay/Queue.hs b/lib/Postdelay/Queue.hs new file mode 100644 index 0000000..ac760ec --- /dev/null +++ b/lib/Postdelay/Queue.hs | |||
@@ -0,0 +1,108 @@ | |||
1 | {-# LANGUAGE RankNTypes, RecordWildCards, ApplicativeDo, FlexibleContexts #-} | ||
2 | |||
3 | module Postdelay.Queue | ||
4 | ( sendmail | ||
5 | , StoredMailF(..) | ||
6 | , StoredMail, StoredMail' | ||
7 | , sendStored, sendStored' | ||
8 | , StoreConfig, storeConfig | ||
9 | , pushStore, popStore | ||
10 | ) where | ||
11 | |||
12 | import Postdelay.Types | ||
13 | |||
14 | import Control.Monad.IO.Class (MonadIO(..)) | ||
15 | import Control.Monad.Catch (handle, MonadThrow(throwM)) | ||
16 | import Control.Monad.Trans.Maybe | ||
17 | import Control.Monad.Reader | ||
18 | import Control.Monad.Trans.Class | ||
19 | import Control.Monad | ||
20 | |||
21 | import System.IO (hClose, stderr) | ||
22 | import System.IO.Error | ||
23 | import GHC.IO.Exception | ||
24 | import System.Process | ||
25 | |||
26 | import Data.ByteString.Lazy as Lazy (ByteString) | ||
27 | import qualified Data.ByteString.Lazy as Lazy.BS | ||
28 | |||
29 | import Foreign.C.Error (Errno(..), ePIPE) | ||
30 | |||
31 | import Data.Bool | ||
32 | import Data.Conduit | ||
33 | import Data.Time | ||
34 | |||
35 | import qualified Options.Applicative as Opt (Parser) | ||
36 | import Options.Applicative hiding (Parser) | ||
37 | |||
38 | |||
39 | data StoredMailF a = StoredMail | ||
40 | { sendDelay :: Delay | ||
41 | , sender :: String | ||
42 | , recipients :: [String] | ||
43 | , message :: a | ||
44 | } | ||
45 | |||
46 | type StoredMail = StoredMailF Lazy.ByteString | ||
47 | type StoredMail' = StoredMailF (IO Lazy.ByteString) | ||
48 | |||
49 | data StoreConfig = StoreConfig | ||
50 | { storeLocation :: FilePath | ||
51 | } | ||
52 | |||
53 | |||
54 | storeConfig :: Opt.Parser StoreConfig | ||
55 | storeConfig = do | ||
56 | storeLocation <- strOption $ mconcat | ||
57 | [ long "spool" | ||
58 | , short 's' | ||
59 | , metavar "PATH" | ||
60 | , help "Where to expect the mail spool" | ||
61 | ] | ||
62 | pure StoreConfig{..} | ||
63 | |||
64 | |||
65 | sendmail' :: MonadIO m | ||
66 | => [String] -- ^ Arguments to sendmail | ||
67 | -> Lazy.ByteString -- ^ Message | ||
68 | -> m () | ||
69 | sendmail' args mail = liftIO . withCreateProcess cp $ \(Just inh) _ _ pHandle -> do | ||
70 | ignoreSigPipe $ do | ||
71 | Lazy.BS.hPutStr inh mail | ||
72 | hClose inh | ||
73 | |||
74 | ret <- waitForProcess pHandle | ||
75 | bool throwM (\_ -> return ()) (ret == ExitSuccess) $ ret | ||
76 | where | ||
77 | cp = (proc "sendmail" args) | ||
78 | { std_in = CreatePipe | ||
79 | } | ||
80 | ignoreSigPipe = handle $ \e -> case e of | ||
81 | IOError { ioe_type = ResourceVanished | ||
82 | , ioe_errno = Just ioe } | ||
83 | | Errno ioe == ePIPE -> return () | ||
84 | _ -> throwM e | ||
85 | |||
86 | sendmail :: MonadIO m | ||
87 | => String -- ^ Sender | ||
88 | -> [String] -- ^ Recipients | ||
89 | -> Lazy.ByteString -- ^ Message | ||
90 | -> m () | ||
91 | sendmail sender recipients = sendmail' $ ["-G", "-i", "-f", sender, "--"] ++ recipients | ||
92 | |||
93 | sendStored :: MonadIO m => StoredMail -> m () | ||
94 | sendStored StoredMail{ sendDelay = Until{..}, ..} = void . runMaybeT $ do | ||
95 | now <- liftIO getCurrentTime | ||
96 | guard $ now >= releaseTime | ||
97 | lift $ sendmail sender recipients message | ||
98 | |||
99 | sendStored' :: MonadIO m => StoredMail' -> m () | ||
100 | sendStored' StoredMail{..} = do | ||
101 | message' <- liftIO message | ||
102 | sendStored StoredMail{ message = message', .. } | ||
103 | |||
104 | popStore :: (MonadIO m, MonadReader StoreConfig m) => Source m StoredMail' | ||
105 | popStore = undefined | ||
106 | |||
107 | pushStore :: (MonadIO m, MonadReader StoreConfig m) => Source m StoredMail -> m () | ||
108 | pushStore = undefined | ||
diff --git a/lib/Postdelay/Scan.hs b/lib/Postdelay/Scan.hs index 7f80818..6355928 100644 --- a/lib/Postdelay/Scan.hs +++ b/lib/Postdelay/Scan.hs | |||
@@ -3,7 +3,6 @@ | |||
3 | module Postdelay.Scan | 3 | module Postdelay.Scan |
4 | ( scan | 4 | ( scan |
5 | , Delay(..) | 5 | , Delay(..) |
6 | , ParseError(..) | ||
7 | ) where | 6 | ) where |
8 | 7 | ||
9 | import Postdelay.Types | 8 | import Postdelay.Types |
diff --git a/lib/Postdelay/TimeSpec.hs b/lib/Postdelay/TimeSpec.hs index 951b1c0..f61d84e 100644 --- a/lib/Postdelay/TimeSpec.hs +++ b/lib/Postdelay/TimeSpec.hs | |||
@@ -124,7 +124,7 @@ dateSpec = label "Date" $ (>> scribe (prio' Default . time) midnight) <$> choice | |||
124 | scribe (prio' Assign . date.days) 1 | 124 | scribe (prio' Assign . date.days) 1 |
125 | ] | 125 | ] |
126 | , try $ do | 126 | , try $ do |
127 | let daySuffix = optional $ choice [ string' "st", string' "nd", string' "rd", string' "th" ] | 127 | let daySuffix = optional $ choice [ string ".", string' "st", string' "nd", string' "rd", string' "th" ] |
128 | (m, d) <- choice [ try $ (,) <$> monthName <* spaces <*> dayNumber False <* daySuffix | 128 | (m, d) <- choice [ try $ (,) <$> monthName <* spaces <*> dayNumber False <* daySuffix |
129 | , try $ flip (,) <$> dayNumber False <* daySuffix <* spaces <*> monthName | 129 | , try $ flip (,) <$> dayNumber False <* daySuffix <* spaces <*> monthName |
130 | ] | 130 | ] |
diff --git a/postdelay.cabal b/postdelay.cabal index ad8f59e..8be804b 100644 --- a/postdelay.cabal +++ b/postdelay.cabal | |||
@@ -19,9 +19,11 @@ cabal-version: >=1.10 | |||
19 | library | 19 | library |
20 | exposed-modules: Postdelay.Scan | 20 | exposed-modules: Postdelay.Scan |
21 | , Postdelay.Types | 21 | , Postdelay.Types |
22 | , Postdelay.Queue | ||
22 | other-modules: Postdelay.TimeSpec | 23 | other-modules: Postdelay.TimeSpec |
23 | , Postdelay.TimeSpec.Units | 24 | , Postdelay.TimeSpec.Units |
24 | , Postdelay.TimeSpec.Utils | 25 | , Postdelay.TimeSpec.Utils |
26 | , Postdelay.PrioMap | ||
25 | build-depends: base >=4.9 && <5 | 27 | build-depends: base >=4.9 && <5 |
26 | , time >=1.6 && <2 | 28 | , time >=1.6 && <2 |
27 | , parsec >=3.1 && <4 | 29 | , parsec >=3.1 && <4 |
@@ -40,6 +42,12 @@ library | |||
40 | , lens-datetime >=0.3 && <1 | 42 | , lens-datetime >=0.3 && <1 |
41 | , data-interval >=1.2 && <2 | 43 | , data-interval >=1.2 && <2 |
42 | , containers >=0.5.7 && <1 | 44 | , containers >=0.5.7 && <1 |
45 | , vector-space >=0.10 && <1 | ||
46 | , process >=1.4 && <2 | ||
47 | , conduit >=1.2 && <2 | ||
48 | , transformers >=0.5 && <1 | ||
49 | , bytestring >=0.10 && <1 | ||
50 | , optparse-applicative >=0.13 && <1 | ||
43 | hs-source-dirs: lib | 51 | hs-source-dirs: lib |
44 | default-language: Haskell2010 | 52 | default-language: Haskell2010 |
45 | 53 | ||
@@ -49,6 +57,11 @@ executable postdelay-simple | |||
49 | -- other-extensions: | 57 | -- other-extensions: |
50 | build-depends: base >=4.9 && <5 | 58 | build-depends: base >=4.9 && <5 |
51 | , postdelay | 59 | , postdelay |
52 | , transformers >=0.5 && <1 | 60 | , case-insensitive >=1.2 && <2 |
61 | , exceptions >=0.8 && <1 | ||
62 | , bytestring >=0.10 && <1 | ||
63 | , conduit >=1.2 && <2 | ||
64 | , optparse-applicative >=0.13 && <1 | ||
65 | , mtl >=2.2 && <3 | ||
53 | hs-source-dirs: src | 66 | hs-source-dirs: src |
54 | default-language: Haskell2010 | 67 | default-language: Haskell2010 |
diff --git a/postdelay.nix b/postdelay.nix index d6fce32..647b2ae 100644 --- a/postdelay.nix +++ b/postdelay.nix | |||
@@ -1,7 +1,8 @@ | |||
1 | { mkDerivation, base, case-insensitive, containers, data-interval | 1 | { mkDerivation, base, bytestring, case-insensitive, conduit |
2 | , exceptions, hsemail, lens, lens-datetime, list-t, megaparsec | 2 | , containers, data-interval, exceptions, hsemail, lens |
3 | , mime, mtl, old-time, parsec, stdenv, time, transformers, tz | 3 | , lens-datetime, list-t, megaparsec, mime, mtl, old-time |
4 | , units, units-defs | 4 | , optparse-applicative, parsec, process, stdenv, time, transformers |
5 | , tz, units, units-defs, vector-space | ||
5 | }: | 6 | }: |
6 | mkDerivation { | 7 | mkDerivation { |
7 | pname = "postdelay"; | 8 | pname = "postdelay"; |
@@ -10,11 +11,15 @@ mkDerivation { | |||
10 | isLibrary = true; | 11 | isLibrary = true; |
11 | isExecutable = true; | 12 | isExecutable = true; |
12 | libraryHaskellDepends = [ | 13 | libraryHaskellDepends = [ |
13 | base case-insensitive containers data-interval exceptions hsemail | 14 | base bytestring case-insensitive conduit containers data-interval |
14 | lens lens-datetime list-t megaparsec mime mtl old-time parsec time | 15 | exceptions hsemail lens lens-datetime list-t megaparsec mime mtl |
15 | tz units units-defs | 16 | old-time optparse-applicative parsec process time transformers tz |
17 | units units-defs vector-space | ||
18 | ]; | ||
19 | executableHaskellDepends = [ | ||
20 | base bytestring case-insensitive conduit exceptions mtl | ||
21 | optparse-applicative | ||
16 | ]; | 22 | ]; |
17 | executableHaskellDepends = [ base transformers ]; | ||
18 | homepage = "https://git.yggdrasil.li/gkleen/pub/postdelay"; | 23 | homepage = "https://git.yggdrasil.li/gkleen/pub/postdelay"; |
19 | description = "A postfix content filter for delaying delivery of mail"; | 24 | description = "A postfix content filter for delaying delivery of mail"; |
20 | license = stdenv.lib.licenses.mit; | 25 | license = stdenv.lib.licenses.mit; |
diff --git a/src/Simple.hs b/src/Simple.hs index f461766..68524a8 100644 --- a/src/Simple.hs +++ b/src/Simple.hs | |||
@@ -1,24 +1,65 @@ | |||
1 | {-# LANGUAGE RecordWildCards, ApplicativeDo, FlexibleContexts #-} | ||
2 | |||
1 | module Main (main) where | 3 | module Main (main) where |
2 | 4 | ||
3 | import Postdelay.Scan | 5 | import Postdelay.Scan |
6 | import Postdelay.Queue | ||
4 | 7 | ||
5 | import Control.Monad.IO.Class | 8 | import Data.Function (on) |
6 | import Control.Monad.Trans.Except | 9 | import Data.List |
10 | import Data.CaseInsensitive (CI) | ||
11 | import qualified Data.CaseInsensitive as CI | ||
7 | 12 | ||
8 | 13 | import Control.Monad.IO.Class (MonadIO(..)) | |
9 | main :: MonadIO m => m () | 14 | import Control.Monad.Catch (MonadCatch) |
15 | import Control.Monad.Reader | ||
16 | |||
17 | import Data.ByteString.Lazy as Lazy (ByteString) | ||
18 | import qualified Data.ByteString.Lazy as Lazy.BS | ||
19 | |||
20 | import Data.ByteString.Lazy.Char8 as Lazy.Char8 (ByteString) | ||
21 | import qualified Data.ByteString.Lazy.Char8 as Lazy.Char8.BS | ||
22 | |||
23 | import Data.Conduit | ||
24 | |||
25 | import System.Environment (getProgName) | ||
26 | |||
27 | import qualified Options.Applicative as Opt (Parser) | ||
28 | import Options.Applicative hiding (Parser) | ||
29 | |||
30 | |||
31 | opts :: Opt.Parser (String, [String], StoreConfig) | ||
32 | opts = do | ||
33 | sConfig <- storeConfig | ||
34 | sender <- strOption $ mconcat | ||
35 | [ long "sender" | ||
36 | , short 'f' | ||
37 | , metavar "SENDER" | ||
38 | ] | ||
39 | recipients <- some . strArgument $ mconcat | ||
40 | [ metavar "RECIPIENTS" | ||
41 | ] | ||
42 | pure (sender, recipients, sConfig) | ||
43 | |||
44 | main :: (MonadIO m, MonadCatch m) => m () | ||
10 | main = do | 45 | main = do |
11 | mailStr <- liftIO getContents | 46 | pName <- liftIO getProgName |
12 | delay <- runExceptT $ scan mailStr | 47 | (sender, recipients, sConfig) <- liftIO . execParser . info (helper <*> opts) $ mconcat |
13 | case delay of | 48 | [ fullDesc |
14 | Left err -> do | 49 | , progDesc "Queue an email for later delivery" |
15 | liftIO . putStrLn $ show err | 50 | , header pName |
16 | sendNow mailStr | 51 | ] |
17 | Right Nothing -> sendNow mailStr | 52 | |
18 | Right (Just d) -> sendLater mailStr d | 53 | flip runReaderT sConfig $ do |
19 | 54 | mailStr <- liftIO getContents | |
20 | sendNow :: MonadIO m => String -> m () | 55 | delay <- scan mailStr |
21 | sendNow = undefined | 56 | let |
22 | 57 | (headers, body) = break (== "") $ lines mailStr | |
23 | sendLater :: MonadIO m => String -> Delay -> m () | 58 | mailStr' = unlines $ filter (not . (isPrefixOf `on` CI.foldCase) "X-Delay:") headers ++ body |
24 | sendLater = undefined | 59 | |
60 | sendNow = sendmail sender recipients $ Lazy.Char8.BS.pack mailStr' | ||
61 | sendLater sendDelay = pushStore . yield $ StoredMail | ||
62 | { message = Lazy.Char8.BS.pack mailStr' | ||
63 | , .. | ||
64 | } | ||
65 | maybe sendNow sendLater delay | ||