module Codec.MIME.String.Flatten (flatten) where

import qualified Codec.Binary.Base64.String as Base64 (encode)
import Codec.MIME.String.Date (FullDate, show_full_date, show_mbox_full_date)
import Codec.MIME.String.EncodedWord (base64_encode)
import qualified Codec.MIME.String.QuotedPrintable as QP (encode)
import Codec.MIME.String.Internal.Utils

import Data.List (intersperse)
import Network.BSD (getHostName)
import System.Locale (defaultTimeLocale)
import System.Random (randomIO)
import System.Time (getClockTime, toCalendarTime, formatCalendarTime)

-- XXX This should take a headers structure
-- Flatten {meta info, message body and list of attachments} to a raw message
flatten :: String -> String -> String -> FullDate
        -> String -> [(String, FilePath)]
        -> IO String
flatten from_name from_email subject date body attachments
 = do msgid <- mk_msgid
      let -- XXX This (\n) could be prettier - check llengths of bits
          from_full = encode_name from_name ++ "\n <" ++ from_email ++ ">"
          boundary = "=:"
          part_start = "\n--" ++ boundary ++ "\n"
          parts_end  = "\n--" ++ boundary ++ "--\n"
          text_content = unlines [
              "Content-type: text/plain; charset=utf-8",
              "Content-transfer-encoding: quoted-printable",
              "Content-Disposition: inline"]
          common_headers = unlines [
              "From " ++ from_email ++ " " ++ show_mbox_full_date date,
              "Date: " ++ show_full_date date,
              "Message-ID: " ++ msgid,
              "From: " ++ from_full,
              "Subject: " ++ mk_subject subject,
              "MIME-Version: 1.0"]
          -- This is overly paranoid
          safe_char c = isAsciiAlphaNum c || (c `elem` " .-_")
          mime_attachment (a, fn)
              = part_start
                -- XXX Can we give a better MIME type than this?
             ++ "Content-type: application/octet-stream\n"
             ++ "Content-transfer-encoding: base64\n"
             ++ "Content-Disposition: attachment; filename=\""
                    ++ (case reverse $ filter safe_char
                                     $ takeWhile ('/' /=)
                                     $ reverse fn of
                            [] -> "unknown"
                            fn' -> fn')
                    ++ "\"\n"
             ++ "\n"
             ++ Base64.encode a
          msg = if single_part
                then common_headers
                  ++ text_content
                  ++ "\n"
                  ++ QP.encode (my_lines body)
                else common_headers
                  ++ "Content-type: multipart/mixed; boundary=\""
                                                ++ boundary ++ "\"\n"
                  ++ "\n"
                  ++ "This is a multi-part message in MIME format.\n"
                  ++ part_start
                  ++ text_content
                  ++ "\n"
                  ++ QP.encode (my_lines body)
                  ++ concatMap mime_attachment attachments
                  ++ parts_end
      return msg
    where single_part = null attachments

-- XXX The IO calls used to use liftIOErr. We could use something similar
-- but require we are called in a MonadError m?
-- XXX We should possibly be including a program name
mk_msgid :: IO String
mk_msgid
 = do hostname <- getHostName
      clock_time <- getClockTime
      calendar_time <- toCalendarTime clock_time
      r <- randomIO
      let timestamp = formatCalendarTime defaultTimeLocale
                                         "%Y%m%d%H%M%S" calendar_time
          lhs = concat $ intersperse "."
                [timestamp, show (r :: Int)]
      return (lhs ++ "@" ++ hostname)

mk_subject :: String -> String
mk_subject subject
 = if can_send_clear subject &&
      length_at_most (76 - length "Subject: ") subject
   then subject
   else encode_string (length "Subject: ") subject

can_send_clear :: String -> Bool
can_send_clear = all isAsciiPrint

length_at_most :: Int -> [a] -> Bool
length_at_most _ [] = True
length_at_most 0 _ = False
length_at_most i (_:xs) = length_at_most (i-1) xs

encode_name :: String -> String
encode_name rn = encode_string (length "From: ") rn

encode_string :: Int -> String -> String
encode_string n s
 = concat $ intersperse "\n " $ map (base64_encode "utf-8") (splits n' s)
    where n' = ((76 - n - length "=?utf-8?B??=") * 3) `div` 4