]> gitweb.michael.orlitzky.com - dead/halcyon.git/blob - src/Mail.hs
Update the default headers to include a Content-Transfer-Encoding: 8bit.
[dead/halcyon.git] / src / Mail.hs
1 -- |Email functions and data types.
2
3 module Mail
4 where
5
6 import Control.Concurrent
7 import Control.Exception (evaluate)
8 import Data.List (intercalate)
9 import System.Exit
10 import System.Process
11 import System.Time (CalendarTime(..), ClockTime, getClockTime, Month, toCalendarTime)
12 import System.IO (hClose, hGetContents, hPutStr)
13
14
15 type Header = String
16
17 -- |A crude model of an RFC821 email message.
18 data Message = Message { headers :: [Header],
19 subject :: String,
20 body :: String,
21 from :: String,
22 to :: String }
23 deriving (Eq)
24
25 -- |The default headers attached to each message.
26 -- The MIME junk is needed for UTF-8 to work properly.
27 -- Note that your mail server should support the 8BITMIME extension.
28 default_headers :: [Header]
29 default_headers = ["MIME-Version: 1.0",
30 "Content-Type: text/plain; charset=UTF-8",
31 "Content-Transfer-Encoding: 8bit"]
32
33 -- |Showing a message will print it in roughly RFC-compliant
34 -- form. This form is sufficient for handing the message off to
35 -- sendmail.
36 instance Show Message where
37 show m =
38 concat [ if (length (headers m) == 0) then "" else (intercalate "\n" (headers m)) ++ "\n",
39 "Subject: " ++ (subject m) ++ "\n",
40 "From: " ++ (from m) ++ "\n",
41 "To: " ++ (to m) ++ "\n",
42 "\n",
43 (body m) ]
44
45
46
47 -- |Pad a string on the left with zeros until the entire string has
48 -- length n.
49 pad_left :: String -> Int -> String
50 pad_left str n
51 | n < (length str) = str
52 | otherwise = (replicate num_zeros '0') ++ str
53 where num_zeros = n - (length str)
54
55
56
57 -- |Formats a month name according to RFC822.
58 format_month :: Month -> String
59 format_month month = take 3 (show month)
60
61
62 -- |Takes an offset from UTC (in seconds) and returns the four-digit
63 -- offset as a 'String' in +hhmm format.
64 format_timezone :: Int -> String
65 format_timezone seconds =
66 sign ++ pad_hh ++ pad_mm
67 where
68 seconds_norm = abs seconds
69 hh = seconds_norm `div` 3600
70 mm = (seconds_norm - (hh*3600)) `div` 60
71 pad_hh = pad_left (show hh) 2
72 pad_mm = pad_left (show mm) 2
73 sign = if seconds < 0 then "-" else "+"
74
75
76 -- |Takes a 'ClockTime' as an argument, and formats it as an RFC822 Date header.
77 --
78 -- See,
79 --
80 -- <http://cr.yp.to/immhf/date.html>
81 --
82 -- for information.
83 format_clocktime :: ClockTime -> IO String
84 format_clocktime ct = do
85 caltime <- (toCalendarTime ct)
86
87 let days = pad_left (show (ctDay caltime)) 2
88 let month = format_month (ctMonth caltime)
89 let year = show $ ctYear caltime
90 let hours = pad_left (show (ctHour caltime)) 2
91 let minutes = pad_left (show (ctMin caltime)) 2
92 let seconds = pad_left (show (ctSec caltime)) 2
93 let timezone = format_timezone (ctTZ caltime)
94
95 return $ concat [(show $ ctWDay caltime) ++ ", ",
96 days ++ " ",
97 month ++ " ",
98 year ++ " ",
99 hours ++ ":",
100 minutes ++ ":",
101 seconds ++ " ",
102 timezone]
103
104
105 -- |Constructs an RFC822 Date header with the current date/time.
106 construct_date_header :: IO String
107 construct_date_header = do
108 date <- getClockTime
109 format_clocktime date
110
111
112
113 -- |Takes a message as an argument, and passes it to the system's
114 -- sendmail binary.
115 sendmail :: Message -> IO (String, String, ExitCode)
116 sendmail message = do
117 let sendmail_args = ["-f",
118 (from message)]
119
120 (inh, outh, errh, ph) <-
121 runInteractiveProcess "/usr/bin/sendmail" sendmail_args Nothing Nothing
122
123 outm <- newEmptyMVar
124 outs <- hGetContents outh
125
126 errm <- newEmptyMVar
127 errs <- hGetContents errh
128
129 _ <- forkIO $ hPutStr inh (show message) >> hClose inh
130 _ <- forkIO $ evaluate (length outs) >> putMVar outm ()
131 _ <- forkIO $ evaluate (length errs) >> putMVar errm ()
132
133 readMVar outm
134 readMVar errm
135
136 ec <- waitForProcess ph
137 return (outs, errs, ec)
138
139
140 -- |The 'sendmail' function returns a three-tuple of its outputs,
141 -- errors, and exit codes. This function pretty-prints one of those
142 -- three-tuples.
143 print_sendmail_result :: (String, String, ExitCode) -> IO ()
144 print_sendmail_result (outs, errs, ec) = do
145 case ec of
146 ExitSuccess -> return ()
147 _ -> putStrLn $ concat ["Output: " ++ outs,
148 "\nErrors: " ++ errs,
149 "\nExit Code: " ++ (show ec)]