module Main (main) where
import Prelude ()
import BasicPrelude
import System.IO
(stdout, stderr, hSetBuffering, BufferMode(LineBuffering))
import Control.Concurrent (threadDelay)
import Control.Error (exceptT)
import Network (PortID (PortNumber))
import Data.Time.Clock (getCurrentTime)
import qualified Focus
import qualified StmContainers.Map as STMMap
import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as UUID
import qualified Data.XML.Types as XML
import qualified Network.Protocol.XMPP as XMPP
import qualified Network.Protocol.XMPP.Internal as XMPP
import qualified Data.MIME as MIME
import qualified Network.Mail.Mime as Mail
import Util
import Router
import Email
newtype RawComponentStanza = RawComponentStanza XML.Element
instance XMPP.Stanza RawComponentStanza where
stanzaTo (RawComponentStanza el) =
XMPP.parseJID =<<
XML.attributeText (s"{jabber:component:accept}to") el
stanzaFrom (RawComponentStanza el) =
XMPP.parseJID =<<
XML.attributeText (s"{jabber:component:accept}from") el
stanzaID (RawComponentStanza el) =
XML.attributeText (s"{jabber:component:accept}id") el
stanzaLang (RawComponentStanza el) =
XML.attributeText (s"xml:lang") el
stanzaPayloads (RawComponentStanza el) = XML.elementChildren el
stanzaToElement (RawComponentStanza el) = el
defaultMessageError :: XML.Element
defaultMessageError = errorPayload "cancel" "undefined-condition"
(s"Unknown error sending message") []
overrideID :: Text -> XML.Element -> XML.Element
overrideID newID el = el {
XML.elementAttributes =
(s"{jabber:component:accept}id", [XML.ContentText newID]) :
XML.elementAttributes el
}
iqSetHandler ::
STMMap.Map (Maybe Text) XMPP.IQ
-> XMPP.JID
-> [XMPP.JID]
-> XMPP.IQ
-> XMPP.XMPP ()
iqSetHandler replyMap componentJid trustedJids iq@XMPP.IQ {
XMPP.iqFrom = Just from,
XMPP.iqTo = Just to,
XMPP.iqPayload = payload
} | to == componentJid && bareJid from `elem` trustedJids = do
uuid <- liftIO UUID.nextRandom
let sid = UUID.toText uuid
atomicUIO $ STMMap.insert iq (Just sid) replyMap
mapM_ XMPP.putStanza $
RawComponentStanza . overrideID sid <$> payload
void $ forkXMPP $ do
liftIO $ threadDelay 2000000
lookupIQ <- atomicUIO $ STMMap.focus
Focus.lookupAndDelete (Just sid) replyMap
forM_ lookupIQ $ \originalIQ ->
XMPP.putStanza $ iqReply Nothing originalIQ
iqSetHandler _ _ _ iq = XMPP.putStanza $ iqError notImplemented iq
messageHandler ::
MIME.Domain
-> XMPP.Message
-> XMPP.XMPP ()
messageHandler fromDomain message = do
now <- liftIO getCurrentTime
forM_ (messageToEmail fromDomain now message) $ \(from, mail) ->
liftIO $ Mail.sendmailCustom "/usr/sbin/sendmail" [
"-t", "-i",
"-f", textToString $ decodeUtf8 $
MIME.renderMailbox from
] (MIME.renderMessage mail)
messageErrorHandler ::
STMMap.Map (Maybe Text) XMPP.IQ
-> XMPP.Message
-> XMPP.XMPP ()
messageErrorHandler replyMap message = do
let errorElement = fromMaybe defaultMessageError $ errorChild message
lookupIQ <- atomicUIO $ STMMap.focus
Focus.lookupAndDelete (XMPP.stanzaID message) replyMap
forM_ lookupIQ $ \originalIQ ->
XMPP.putStanza $ iqError errorElement originalIQ
-- TODO: else, manual bounce?
main :: IO ()
main = do
hSetBuffering stdout LineBuffering
hSetBuffering stderr LineBuffering
(componentJidTxt:host:portTxt:secret:trustedJidsTxt) <- getArgs
let Right (MIME.Mailbox _ (MIME.AddrSpec _ emailDomain)) =
MIME.parse (MIME.mailbox MIME.defaultCharsets)
(s"boop@" ++ encodeUtf8 componentJidTxt)
let Just componentJid = XMPP.parseJID componentJidTxt
let Just trustedJids = mapM XMPP.parseJID trustedJidsTxt
let port = PortNumber $ read portTxt
let server = XMPP.Server componentJid (textToString host) port
replyMap <- STMMap.newIO
exceptT print return $ runRoutedComponent server secret $ defaultRoutes{
iqSetRoute =
iqSetHandler replyMap componentJid trustedJids,
messageNormalRoute = messageHandler emailDomain,
messageChatRoute = messageHandler emailDomain,
messageErrorRoute =
messageErrorHandler replyMap
}