module Main (main) where
import Prelude ()
import BasicPrelude
import System.IO
(stdout, stderr, hSetBuffering, BufferMode(LineBuffering))
import Control.Concurrent (threadDelay)
import Control.Concurrent.STM (STM)
import Control.Error (exceptT, headZ)
import Control.Lens (over, set, at, _Right, traverseOf)
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 Data.MIME.EncodedWord as MIME
import Email
import IQManager
import Router
import Util
import VCard
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
addVCardData :: VCard -> MIME.MIMEMessage -> MIME.MIMEMessage
addVCardData vcard =
set (MIME.headers . at (s"X-URL"))
(MIME.encodeEncodedWords <$> headZ (url vcard)) .
over (MIME.headerFrom MIME.defaultCharsets) (map
(\(MIME.Mailbox name addr) ->
MIME.Mailbox (nickname vcard <|> fn vcard <|> name) addr
)
)
fetchAndAddVCardData ::
(XMPP.IQ -> XMPP.XMPP (STM (Maybe XMPP.IQ)))
-> MIME.MIMEMessage
-> XMPP.JID
-> XMPP.XMPP MIME.MIMEMessage
fetchAndAddVCardData sendIQ email jid =
(`addVCardData` email) . maybe emptyVCard parseVCard <$>
(atomicUIO =<< sendIQ (vcardRequest jid))
messageHandler ::
MIME.Domain
-> (XMPP.IQ -> XMPP.XMPP (STM (Maybe XMPP.IQ)))
-> XMPP.Message
-> XMPP.XMPP ()
messageHandler fromDomain sendIQ message = do
now <- liftIO getCurrentTime
either XMPP.putStanza sendEmail =<< traverseOf (_Right . emailMessage')
(\msg ->
maybe (return msg)
(fetchAndAddVCardData sendIQ msg)
(XMPP.messageFrom message)
)
(messageToEmail fromDomain now message)
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 $ do
(sendIQ, iqReceived) <- iqManager
return $ defaultRoutes {
iqSetRoute =
iqSetHandler replyMap componentJid trustedJids,
iqResultRoute = iqReceived,
iqErrorRoute = iqReceived,
messageNormalRoute = messageHandler emailDomain sendIQ,
messageChatRoute = messageHandler emailDomain sendIQ,
messageErrorRoute = messageErrorHandler replyMap
}