X-Git-Url: http://gitweb.michael.orlitzky.com/?p=dead%2Fhalcyon.git;a=blobdiff_plain;f=src%2FMail.hs;h=dcb686c4837f349ae9d29e367c5f3094f524484d;hp=2da56639667b4b2b63d387aaab601934091f5f7e;hb=aa76db464725dace34b87f452f9ebb9675226e40;hpb=230072d26d55aed92737308aa04ce8a0daa0b71a diff --git a/src/Mail.hs b/src/Mail.hs index 2da5663..dcb686c 100644 --- a/src/Mail.hs +++ b/src/Mail.hs @@ -1,103 +1,166 @@ --- |Email functions and data types. +-- | Email functions and data types. -module Mail +module Mail ( + Message(..), + print_sendmail_result, + rfc822_now, + sendmail ) where -import Control.Concurrent -import Control.Exception (evaluate) -import Data.List (intercalate) -import Data.Time (formatTime, getZonedTime) -import System.Exit -import System.Locale (defaultTimeLocale, rfc822DateFormat) -import System.Process -import System.IO (hClose, hGetContents, hPutStr) +import Control.Concurrent ( forkIO, newEmptyMVar, putMVar, readMVar ) +import Control.Exception ( evaluate ) +import Control.Monad ( liftM ) +import Data.Time ( formatTime, getZonedTime ) +import System.Console.CmdArgs.Default ( Default(..) ) +import System.Exit ( ExitCode(..) ) +import System.Locale ( defaultTimeLocale, rfc822DateFormat ) +import System.Process ( runInteractiveProcess, waitForProcess ) +import System.IO ( hClose, hGetContents, hPutStr ) type Header = String --- |A crude model of an RFC821 email message. -data Message = Message { headers :: [Header], - subject :: String, - body :: String, - from :: String, - to :: String } - deriving (Eq) - --- |The default headers attached to each message. --- The MIME junk is needed for UTF-8 to work properly. --- Note that your mail server should support the 8BITMIME extension. +-- | The default headers attached to each message. The MIME junk is +-- needed for UTF-8 to work properly. Note that your mail server +-- should support the 8BITMIME extension. +-- default_headers :: [Header] default_headers = ["MIME-Version: 1.0", "Content-Type: text/plain; charset=UTF-8", "Content-Transfer-Encoding: 8bit"] --- |Showing a message will print it in roughly RFC-compliant --- form. This form is sufficient for handing the message off to --- sendmail. -instance Show Message where - show m = - concat [ if (length (headers m) == 0) then "" else (intercalate "\n" (headers m)) ++ "\n", - "Subject: " ++ (subject m) ++ "\n", - "From: " ++ (from m) ++ "\n", - "To: " ++ (to m) ++ "\n", - "\n", - (body m) ] +-- | A crude model of an RFC822 email message. +-- +data Message = Message { headers :: [Header], + subject :: String, + body :: String, + from :: String, + to :: String } + deriving (Eq) --- |Pad a string on the left with zeros until the entire string has --- length n. -pad_left :: String -> Int -> String -pad_left str n - | n < (length str) = str - | otherwise = (replicate num_zeros '0') ++ str - where num_zeros = n - (length str) +instance Default Message where + -- | Construct a message with all of its fields set to their + -- default values. + -- + def = Message default_headers def def def def + + +-- | Print a 'Message' in roughly RFC-compliant form. This form is +-- sufficient for handing the message off to sendmail (or compatible). +-- +-- Examples: +-- +-- >>> let hs = default_headers +-- >>> let s = "Save up to 20% on garbage!" +-- >>> let b = "Just kidding, now you have a virus!" +-- >>> let f = "savings5000@impenetrable.example" +-- >>> let t = "everyone@everywhere.example" +-- >>> let msg = Message hs s b f t +-- >>> putStrLn $ to_rfc822 msg +-- MIME-Version: 1.0 +-- Content-Type: text/plain; charset=UTF-8 +-- Content-Transfer-Encoding: 8bit +-- Subject: Save up to 20% on garbage! +-- From: savings5000@impenetrable.example +-- To: everyone@everywhere.example +-- +-- Just kidding, now you have a virus! +-- +to_rfc822 :: Message -> String +to_rfc822 m = + concat [ formatted_headers, + "Subject: " ++ (subject m) ++ "\n", + "From: " ++ (from m) ++ "\n", + "To: " ++ (to m) ++ "\n", + "\n", + (body m) ] + where + formatted_headers = + if null (headers m) + then "" + else unlines (headers m) -- | Constructs a 'String' in RFC822 date format for the current -- date/time. +-- rfc822_now :: IO String -rfc822_now = do - date <- getZonedTime - return $ formatTime defaultTimeLocale rfc822DateFormat date +rfc822_now = + liftM (formatTime defaultTimeLocale rfc822DateFormat) getZonedTime + --- |Takes a message as an argument, and passes it to the system's --- sendmail binary. -sendmail :: Message -> IO (String, String, ExitCode) -sendmail message = do +-- | Takes a message as an argument, and passes it to the system's +-- sendmail (or compatible) binary. +-- +sendmail :: FilePath -> Message -> IO (String, String, ExitCode) +sendmail sendmail_path message = do + -- The arguments we pass to sendmail "on the command line" let sendmail_args = ["-f", - (from message)] + (from message), + (to message)] + -- Run the sendmail process, passing it our sendmail_args. We'll get + -- back a bunch of handles, std{in,out,err} and one for the process + -- itself. (inh, outh, errh, ph) <- - runInteractiveProcess "/usr/bin/sendmail" sendmail_args Nothing Nothing + runInteractiveProcess sendmail_path sendmail_args Nothing Nothing + -- Create mvars for stdout and stderr, then collect their contents. outm <- newEmptyMVar outs <- hGetContents outh errm <- newEmptyMVar errs <- hGetContents errh - _ <- forkIO $ hPutStr inh (show message) >> hClose inh + -- Pass the message to sendmail on stdin + _ <- forkIO $ hPutStr inh (to_rfc822 message) >> hClose inh + + -- Fork threads that will read stdout/stderr respectively, and then + -- stick a dummy unit value in the mvars we created. _ <- forkIO $ evaluate (length outs) >> putMVar outm () _ <- forkIO $ evaluate (length errs) >> putMVar errm () + -- Now wait for the dummy variables to show up in the mvars. This + -- will occur only after (length outs) and (length errs) have been + -- evaluated, which can happen only after we've read them entirely. readMVar outm readMVar errm + -- Now wait for the process to finish and return its exit code along + -- with the output that we collected. ec <- waitForProcess ph return (outs, errs, ec) --- |The 'sendmail' function returns a three-tuple of its outputs, --- errors, and exit codes. This function pretty-prints one of those --- three-tuples. +-- | The 'sendmail' function returns a three-tuple of its outputs, +-- errors, and exit codes. This function pretty-prints one of those +-- three-tuples. +-- +-- If the exit code indicates success, we don't bother to print +-- anything (silence is golden!), but otherwise the contents of both +-- stdout and stderr will be printed. +-- +-- Examples: +-- +-- >>> let r = ("some output", "no errors", ExitSuccess) +-- >>> print_sendmail_result r +-- +-- >>> let r = ("some output", "lots of errors", ExitFailure 1) +-- >>> print_sendmail_result r +-- Output: some output +-- Errors: lots of errors +-- Exit Code: 1 +-- print_sendmail_result :: (String, String, ExitCode) -> IO () -print_sendmail_result (outs, errs, ec) = do - case ec of - ExitSuccess -> return () - _ -> putStrLn $ concat ["Output: " ++ outs, - "\nErrors: " ++ errs, - "\nExit Code: " ++ (show ec)] +print_sendmail_result (outs, errs, ec) = + case ec of + ExitSuccess -> return () + ExitFailure (code) -> + putStrLn $ concat ["Output: " ++ outs, + "\nErrors: " ++ errs, + "\nExit Code: " ++ (show code)]