]> gitweb.michael.orlitzky.com - dead/halcyon.git/blobdiff - src/Mail.hs
Disable TLS certificate verification to make it more worky.
[dead/halcyon.git] / src / Mail.hs
index 03b7284665fa40b742cc6d36fa3a9209c2a6c838..dcb686c4837f349ae9d29e367c5f3094f524484d 100644 (file)
--- |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 System.Exit
-import System.Process
-import System.Time (CalendarTime(..), ClockTime, getClockTime, Month, toCalendarTime)
-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
 
 
--- |Formats a month name according to RFC822.
-format_month :: Month -> String
-format_month month = take 3 (show month)
+-- | 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
+--   <BLANKLINE>
+--   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 =
+  liftM (formatTime defaultTimeLocale rfc822DateFormat) getZonedTime
 
 
--- |Takes an offset from UTC (in seconds) and returns the four-digit
--- offset as a 'String' in +hhmm format.
-format_timezone :: Int -> String
-format_timezone seconds =
-    sign ++ pad_hh ++ pad_mm
-    where
-      seconds_norm = abs seconds
-      hh = seconds_norm `div` 3600
-      mm = (seconds_norm - (hh*3600)) `div` 60
-      pad_hh = pad_left (show hh) 2
-      pad_mm = pad_left (show mm) 2
-      sign = if seconds < 0 then "-" else "+"
 
 
--- |Takes a 'ClockTime' as an argument, and formats it as an RFC822 Date header.
---
--- See,
+-- | Takes a message as an argument, and passes it to the system's
+--   sendmail (or compatible) binary.
 --
---   <http://cr.yp.to/immhf/date.html>
---
--- for information.
-format_clocktime :: ClockTime -> IO String
-format_clocktime ct = do
-  caltime <- (toCalendarTime ct)
-
-  let days = pad_left (show (ctDay caltime)) 2
-  let month = format_month (ctMonth caltime)
-  let year = show $ ctYear caltime
-  let hours = pad_left (show (ctHour caltime)) 2
-  let minutes = pad_left (show (ctMin caltime)) 2
-  let seconds = pad_left (show (ctSec caltime)) 2
-  let timezone = format_timezone (ctTZ caltime)
-
-  return $ concat [(show $ ctWDay caltime) ++ ", ",
-                   days ++ " ",
-                   month ++ " ",
-                   year ++ " ",
-                   hours ++ ":",
-                   minutes ++ ":",
-                   seconds ++ " ",
-                   timezone]
-
-
--- |Constructs an RFC822 Date header with the current date/time.
-construct_date_header :: IO String
-construct_date_header = do
-  date <- getClockTime
-  format_clocktime date
-
-
-
--- |Takes a message as an argument, and passes it to the system's
--- sendmail binary.
-sendmail :: Message -> IO (String, String, ExitCode)
-sendmail message = do
+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)]