~singpolyma/cheogram-smtp

43c43efbbc4034a600c050e4950218516ea27aaa — Stephen Paul Weber 2 years ago 3464bca
Implement Message-ID and References headers

Now when conversing with an XMPP client that supports <thread> the
emails are threaded correctly.
3 files changed, 370 insertions(+), 26 deletions(-)

M Email.hs
M incoming-email.hs
M test/EmailTest.hs
M Email.hs => Email.hs +91 -13
@@ 3,10 3,13 @@ module Email where
import BasicPrelude
import Prelude ()
import Data.Char                                 (isAscii, isAlphaNum)
import Control.Error                             (headZ, hush)
import Data.Functor                              ((<&>))
import Control.Error                             (headZ, lastZ, justZ, hush)
import Data.Time.Clock                           (UTCTime)
import Data.Time.Format                          (formatTime, defaultTimeLocale)
import Control.Lens 
	(Const, Leftmost, filtered, firstOf, view, _Right, set, at)
import qualified Data.ByteString.Char8           as C8
import qualified Data.Text                       as T
import qualified Data.Attoparsec.ByteString.Lazy as Atto
import qualified Data.MIME                       as MIME


@@ 57,9 60,7 @@ plainTextBody = MIME.entities . filtered isTextPlain . getEmailBody

mailboxNode :: MIME.Mailbox -> Text
mailboxNode (MIME.Mailbox _ (MIME.AddrSpec local _)) =
	fromString $ URI.unEscapeString $
	map (\c -> if c == '=' then '%' else c) $
	textToString $ decodeUtf8 local
	equalsDecode local

mailboxToJID :: Text -> MIME.Mailbox -> Maybe XMPP.JID
mailboxToJID domain (MIME.Mailbox _ addrspec) =


@@ 73,11 74,19 @@ unescapedInEmailLocalpart c = isAscii c &&
	(isAlphaNum c || c `elem` "#$&'*+-/?^_`{|}~")

jidToLocalpart :: XMPP.JID -> ByteString
jidToLocalpart jid = encodeUtf8 $ fromString $
	map (\c -> if c == '%' then '=' else c) $
	URI.escapeURIString unescapedInEmailLocalpart bareStr
	where
	bareStr = textToString $ bareTxt jid
jidToLocalpart = encodeUtf8 . fromString . equalsEncode . bareTxt

equalsEncode :: Text -> String
equalsEncode =
	map (\c -> if c == '%' then '=' else c) .
	URI.escapeURIString unescapedInEmailLocalpart .
	textToString

equalsDecode :: ByteString -> Text
equalsDecode =
	fromString . URI.unEscapeString .
	map (\c -> if c == '=' then '%' else c) .
	textToString . decodeUtf8

jidToMailbox :: XMPP.JID -> MIME.Domain -> MIME.Mailbox
jidToMailbox jid domain = MIME.Mailbox (Just $ bareTxt jid) $


@@ 98,19 107,65 @@ emailToOriginID email = fmap originID $ hush . MIME.parse messageID =<<
	originID msgid = XML.Element (s"{urn:xmpp:sid:0}origin-id")
		[(s"{urn:xmpp:sid:0}id", [XML.ContentText msgid])] []

extractThreadFromRef :: Text -> Atto.Parser Text
extractThreadFromRef domain = fmap equalsDecode $
	Atto.satisfy (==0x3C) *>
	Atto.skipWhile (`notElem`[0x2E,0x3E,0x20]) *>
	Atto.satisfy (==0x2E) *>
	Atto.skipWhile (`notElem`[0x2E,0x3E,0x20]) *>
	Atto.satisfy (==0x2E) *>
	Atto.takeTill (`elem`[0x40,0x3E,0x20]) <*
	Atto.satisfy (==0x40) <*
	Atto.string (encodeUtf8 domain) <*
	Atto.satisfy (==0x3E)

extractThreadFromRefs :: Text -> Atto.Parser Text
extractThreadFromRefs domain =
	justZ =<< lastZ =<<
	Atto.sepBy
		((Just <$> extractThreadFromRef domain) <|> skipWord)
		(Atto.many1 $ Atto.satisfy (==0x20))
	where
	skipWord = Atto.skipWhile (/=0x20) *> pure Nothing

limitReferencesLength :: ByteString -> ByteString
limitReferencesLength refs = C8.unwords $ limit (C8.words refs)
	where
	limit (x:xs) = x : drop (length xs - 4) xs
	limit [] = []

emailToThread :: Text -> MIME.MIMEMessage -> Maybe XML.Element
emailToThread domain email = thread <&> \threadID ->
	XML.Element (s"{jabber:component:accept}thread")
	parent
	[XML.NodeContent $ XML.ContentText $ decodeUtf8 threadID]
	where
	parent =
		maybeToList $
		fmap (
			(,) (s"{jabber:component:accept}parent") .
			(:[]) . XML.ContentText
		) $
		mfilter (/= s"\0")
		(hush . MIME.parse (extractThreadFromRefs domain) =<< refs)
	thread = fmap (s"References: "++) $
		((\x y -> x ++ s" " ++ y) <$> refs <*> msgid) <|> msgid
	msgid = firstOf (MIME.headers . MIME.header (s"message-id")) email
	refs = firstOf (MIME.headers . MIME.header (s"references")) email

emailToStanza ::
	   (MIME.Mailbox -> Maybe XMPP.JID)
	   Text
	-> MIME.MIMEMessage
	-> XMPP.Message
emailToStanza toJid email =
emailToStanza domain email =
	(XMPP.emptyMessage $ emailToMessageType email) {
		XMPP.messageFrom = toJid fromMailbox,
		XMPP.messageFrom = mailboxToJID domain fromMailbox,
		XMPP.messagePayloads = [
			XML.Element (s"{jabber:component:accept}subject") []
				[XML.NodeContent $ XML.ContentText subject],
			XML.Element (s"{jabber:component:accept}body") []
				[XML.NodeContent $ XML.ContentText textBody]
		] ++ nick ++ mid
		] ++ nick ++ mid ++ maybeToList (emailToThread domain email)
	}
	where
	mid = maybeToList $ emailToOriginID email


@@ 134,6 189,25 @@ typeHeaders XMPP.Message { XMPP.messageType = XMPP.MessageChat } =
	set (MIME.headers . at (s"Chat-Version")) (Just $ s"1.0")
typeHeaders _ = id

mkMessageID :: MIME.Domain -> UTCTime -> XMPP.Message -> ByteString
mkMessageID domain now message = ((s"<" ++) . (++ s">")) $
	MIME.renderAddressSpec $ flip MIME.AddrSpec domain
	(fromString $ time ++ "." ++ sid ++ "." ++ thread)
	where
	sid = fromMaybe "=00" $ equalsEncode <$> XMPP.stanzaID message
	thread =
		fromMaybe "=00" $
		fmap (equalsEncode . mconcat . XML.elementText) $
		child (s"{jabber:component:accept}thread") message
	time = formatTime defaultTimeLocale "%s" now

mkReferences :: XMPP.Message -> Maybe ByteString
mkReferences =
	fmap (limitReferencesLength . encodeUtf8) .
	(T.stripPrefix (s"References: ") =<<) .
	fmap (mconcat . XML.elementText) .
	child (s"{jabber:component:accept}thread")

messageToEmail ::
	   MIME.Domain
	-> UTCTime


@@ 145,6 219,8 @@ messageToEmail fromDomain now message@XMPP.Message {
	} | Just bodyTxt <- getBody message,
	    Right toAddress <- parsedToNode =
		Just (fromMailbox,
			set (MIME.headers . at (s"Message-ID")) (Just mid) $
			set (MIME.headers . at (s"References")) refs $
			typeHeaders message $
			set (MIME.headerTo MIME.defaultCharsets) [toAddress] $
			setFrom $


@@ 154,6 230,8 @@ messageToEmail fromDomain now message@XMPP.Message {
			MIME.createTextPlainMessage bodyTxt
		)
	where
	mid = mkMessageID fromDomain now message
	refs = mkReferences message
	jidHeader = Just $ MIME.encodeEncodedWords (bareTxt from)
	dateHeader = fromMaybe now $ parseXMPPTime =<<
		XML.attributeText (s"{urn:xmpp:delay}stamp") =<<

M incoming-email.hs => incoming-email.hs +1 -1
@@ 41,7 41,7 @@ main = do
	input <- LByteString.getContents
	let Right email = MIME.parse messageOptionalMboxFrom input
	let messages = recipientJids <&> \recipientJid ->
		(emailToStanza (mailboxToJID domain) email) {
		(emailToStanza domain email) {
			XMPP.messageTo = Just recipientJid
		}


M test/EmailTest.hs => test/EmailTest.hs +278 -12
@@ 4,6 4,7 @@ import Prelude ()
import BasicPrelude
import Test.Tasty.HUnit
import Test.QuickCheck.Instances ()
import qualified Data.Time.Format as Time
import qualified Data.MIME as MIME
import qualified Data.XML.Types as XML
import qualified Network.Protocol.XMPP as XMPP


@@ 39,7 40,7 @@ unit_mailboxToJID =

unit_emailToStanzaSimple :: IO ()
unit_emailToStanzaSimple =
	show (emailToStanza (mailboxToJID $ s"gateway.example.com") message)
	show (emailToStanza (s"gateway.example.com") message)
	@?=
	show (XMPP.emptyMessage XMPP.MessageNormal) {
		XMPP.messageFrom =


@@ 58,7 59,11 @@ unit_emailToStanzaSimple =
			XML.Element (s"{urn:xmpp:sid:0}origin-id") [(
				s"{urn:xmpp:sid:0}id",
				[XML.ContentText $ s"boop-id@ids.example.com"]
			)] []
			)] [],
			XML.Element (s"{jabber:component:accept}thread") [] [
				XML.NodeContent $ XML.ContentText $
				s"References: <boop-id@ids.example.com>"
			]
		]
	}
	where


@@ 72,7 77,7 @@ unit_emailToStanzaSimple =

unit_emailToStanzaChat :: IO ()
unit_emailToStanzaChat =
	show (emailToStanza (mailboxToJID $ s"gateway.example.com") message)
	show (emailToStanza (s"gateway.example.com") message)
	@?=
	show (XMPP.emptyMessage XMPP.MessageChat) {
		XMPP.messageFrom =


@@ 97,7 102,7 @@ unit_emailToStanzaChat =

unit_emailToStanzUTF8Subject :: IO ()
unit_emailToStanzUTF8Subject =
	show (emailToStanza (mailboxToJID $ s"gateway.example.com") message)
	show (emailToStanza (s"gateway.example.com") message)
	@?=
	show (XMPP.emptyMessage XMPP.MessageNormal) {
		XMPP.messageFrom =


@@ 117,6 122,142 @@ unit_emailToStanzUTF8Subject =
	\\n\
	\Hello\n"

unit_emailToStanzaReply :: IO ()
unit_emailToStanzaReply =
	show (emailToStanza (s"gateway.example.com") message)
	@?=
	show (XMPP.emptyMessage XMPP.MessageNormal) {
		XMPP.messageFrom =
			XMPP.parseJID $ s"f\\40example.com@gateway.example.com",
		XMPP.messagePayloads = [
			XML.Element (s"{jabber:component:accept}subject") [] [
				XML.NodeContent $ XML.ContentText $ s"subject"
			],
			XML.Element (s"{jabber:component:accept}body") [] [
				XML.NodeContent $ XML.ContentText $ s"Hello\n"
			],
			XML.Element (s"{urn:xmpp:sid:0}origin-id") [(
				s"{urn:xmpp:sid:0}id",
				[XML.ContentText $ s"abc@example.com"]
			)] [],
			XML.Element (s"{jabber:component:accept}thread") [(
				s"{jabber:component:accept}parent",
				[XML.ContentText $ s"athread"]
			)] [
				XML.NodeContent $ XML.ContentText $
				s"References: \
				\<123.456.athread@gateway.example.com> \
				\<abc@example.com>"
			]
		]
	}
	where
	Right message = MIME.parse (MIME.message MIME.mime) email
	email = encodeUtf8 $ s"To: to@example.com\n\
	\From: f@example.com\n\
	\Subject: subject\n\
	\Message-ID: <abc@example.com>\n\
	\References: <123.456.athread@gateway.example.com>\n\
	\\n\
	\Hello\n"

unit_emailToStanzaReplyNulThread :: IO ()
unit_emailToStanzaReplyNulThread =
	show (emailToStanza (s"gateway.example.com") message)
	@?=
	show (XMPP.emptyMessage XMPP.MessageNormal) {
		XMPP.messageFrom =
			XMPP.parseJID $ s"f\\40example.com@gateway.example.com",
		XMPP.messagePayloads = [
			XML.Element (s"{jabber:component:accept}subject") [] [
				XML.NodeContent $ XML.ContentText $ s"subject"
			],
			XML.Element (s"{jabber:component:accept}body") [] [
				XML.NodeContent $ XML.ContentText $ s"Hello\n"
			],
			XML.Element (s"{urn:xmpp:sid:0}origin-id") [(
				s"{urn:xmpp:sid:0}id",
				[XML.ContentText $ s"abc@example.com"]
			)] [],
			XML.Element (s"{jabber:component:accept}thread") [] [
				XML.NodeContent $ XML.ContentText $
				s"References: \
				\<123.456.=00@gateway.example.com> \
				\<abc@example.com>"
			]
		]
	}
	where
	Right message = MIME.parse (MIME.message MIME.mime) email
	email = encodeUtf8 $ s"To: to@example.com\n\
	\From: f@example.com\n\
	\Subject: subject\n\
	\Message-ID: <abc@example.com>\n\
	\References: <123.456.=00@gateway.example.com>\n\
	\\n\
	\Hello\n"

unit_emailToStanzaDeepReply:: IO ()
unit_emailToStanzaDeepReply =
	show (emailToStanza (s"gateway.example.com") message)
	@?=
	show (XMPP.emptyMessage XMPP.MessageNormal) {
		XMPP.messageFrom =
			XMPP.parseJID $ s"f\\40example.com@gateway.example.com",
		XMPP.messagePayloads = [
			XML.Element (s"{jabber:component:accept}subject") [] [
				XML.NodeContent $ XML.ContentText $ s"subject"
			],
			XML.Element (s"{jabber:component:accept}body") [] [
				XML.NodeContent $ XML.ContentText $ s"Hello\n"
			],
			XML.Element (s"{urn:xmpp:sid:0}origin-id") [(
				s"{urn:xmpp:sid:0}id",
				[XML.ContentText $ s"abc@example.com"]
			)] [],
			XML.Element (s"{jabber:component:accept}thread") [(
				s"{jabber:component:accept}parent",
				[XML.ContentText $ s"References: <1583335391.\
					\7d84bbbf-4dd8-42f7-81cc-d7f4ffa06609.\
					\exBUAYVLbCAwUgAUpONVhfirfwVfAUZf\
					\@gateway.example.com>\
					\ <20200304152333.GA4768@example.com>"
				]
			)] [
				XML.NodeContent $ XML.ContentText $
				s"References: <1583335391.7d84bbbf-4dd8-42f7-\
					\81cc-d7f4ffa06609.exBUAYVLbCAwUgAUpON\
					\VhfirfwVfAUZf@gateway.example.com>\
					\ <20200304152333.GA4768@example.com>\
					\ <1583335474.cdc32108-582b-4256-a5b0-\
					\09c4d17af3f8.References=3A=20=3C15833\
					\35391=2E7d84bbbf-4dd8-42f7-81cc-d7f4f\
					\fa06609=2EexBUAYVLbCAwUgAUpONVhfirfwV\
					\fAUZf=40gateway=2Eexample=2Ecom\
					\=3E=20=3C20200304152333=2EGA4768=40\
					\example=2Ecom=3E@gateway.example.com>\
					\ <abc@example.com>"
			]
		]
	}
	where
	Right message = MIME.parse (MIME.message MIME.mime) email
	email = encodeUtf8 $ s"To: to@example.com\n\
	\From: f@example.com\n\
	\Subject: subject\n\
	\Message-ID: <abc@example.com>\n\
	\References: <1583335391.7d84bbbf-4dd8-42f7-81cc-d7f4ffa06609\
		\.exBUAYVLbCAwUgAUpONVhfirfwVfAUZf@gateway.example.com>\n\
		\ <20200304152333.GA4768@example.com>\n\
		\ <1583335474.cdc32108-582b-4256-a5b0-09c4d17af3f8.References\
		\=3A=20=3C1583335391=2E7d84bbbf-4dd8-42f7-81cc-d7f4ffa06609\
		\=2EexBUAYVLbCAwUgAUpONVhfirfwVfAUZf=40gateway=2Eexample=2Ecom\
		\=3E=20=3C20200304152333=2EGA4768=40example=2Ecom=3E\
		\@gateway.example.com>\n\
	\\n\
	\Hello\n"


unit_messageToEmailChat :: IO ()
unit_messageToEmailChat =
	fmap (MIME.renderMessage . snd) (


@@ 129,7 270,9 @@ unit_messageToEmailChat =
	Just email
	where
	Just date = parseXMPPTime (s"1990-01-01T00:00:00Z")
	email = s"MIME-Version: 1.0\r\n\
	timestamp = Time.formatTime Time.defaultTimeLocale "%s" date
	email = s$"MIME-Version: 1.0\r\n\
	\Message-ID: <" ++ timestamp ++ ".=00.=00@gateway.example.com>\r\n\
	\Chat-Version: 1.0\r\n\
	\To: t@example.com\r\n\
	\From: =?utf-8?Q?=E4=B8=96@example.com?=\r\n\


@@ 147,7 290,7 @@ unit_messageToEmailChat =
			XMPP.parseJID $ s"t\\40example.com@gateway.example.com",
		XMPP.messageFrom = XMPP.parseJID $ s"世@example.com",
		XMPP.messagePayloads = [
			XML.Element (fromString "{jabber:component:accept}body")
			XML.Element (s"{jabber:component:accept}body")
			[] [
				XML.NodeContent $ XML.ContentText $
					s"世界\n.\n"


@@ 167,7 310,9 @@ unit_messageToEmailWithSubject =
	Just email
	where
	Just date = parseXMPPTime (s"1990-01-01T00:00:00Z")
	email = s"MIME-Version: 1.0\r\n\
	timestamp = Time.formatTime Time.defaultTimeLocale "%s" date
	email = s$"MIME-Version: 1.0\r\n\
	\Message-ID: <" ++ timestamp ++ ".123.=00@gateway.example.com>\r\n\
	\To: t@example.com\r\n\
	\From: \"f@example.com\" <f=40example=2Ecom@gateway.example.com>\r\n\
	\Jabber-ID: f@example.com\r\n\


@@ 179,17 324,18 @@ unit_messageToEmailWithSubject =
	\\r\n\
	\5LiW55WMCi4K\r\n"
	message = (XMPP.emptyMessage XMPP.MessageNormal) {
		XMPP.messageID = Just $ s"123",
		XMPP.messageTo =
			XMPP.parseJID $ s"t\\40example.com@gateway.example.com",
		XMPP.messageFrom = XMPP.parseJID $ s"f@example.com",
		XMPP.messagePayloads = [
			XML.Element
			(fromString "{jabber:component:accept}subject") [] [
			(s"{jabber:component:accept}subject") [] [
				XML.NodeContent $ XML.ContentText $
					s"世界"
			],
			XML.Element
			(fromString "{jabber:component:accept}body") [] [
			(s"{jabber:component:accept}body") [] [
				XML.NodeContent $ XML.ContentText $
					s"世界\n.\n"
			]


@@ 208,7 354,9 @@ unit_messageToEmailWithDelay =
	Just email
	where
	Just date = parseXMPPTime (s"1990-01-01T00:00:00Z")
	email = s"MIME-Version: 1.0\r\n\
	timestamp = Time.formatTime Time.defaultTimeLocale "%s" date
	email = s$"MIME-Version: 1.0\r\n\
	\Message-ID: <" ++ timestamp ++ ".=00.=00@gateway.example.com>\r\n\
	\To: t@example.com\r\n\
	\From: \"f@example.com\" <f=40example=2Ecom@gateway.example.com>\r\n\
	\Jabber-ID: f@example.com\r\n\


@@ 223,11 371,129 @@ unit_messageToEmailWithDelay =
			XMPP.parseJID $ s"t\\40example.com@gateway.example.com",
		XMPP.messageFrom = XMPP.parseJID $ s"f@example.com",
		XMPP.messagePayloads = [
			XML.Element (fromString "{urn:xmpp:delay}delay") [(
			XML.Element (s"{urn:xmpp:delay}delay") [(
				s"{urn:xmpp:delay}stamp",
				[XML.ContentText $ s"2009-02-22T00:10:00Z"]
			)] [],
			XML.Element (fromString "{jabber:component:accept}body")
			XML.Element (s"{jabber:component:accept}body")
			[] [XML.NodeContent $ XML.ContentText $ s"世界\n.\n"]
		]
	}

unit_messageToEmailWithThread :: IO ()
unit_messageToEmailWithThread =
	fmap (MIME.renderMessage . snd) (
		messageToEmail
		(MIME.DomainLiteral $ encodeUtf8 $ s"gateway.example.com")
		date
		message
	)
	@?=
	Just email
	where
	Just date = parseXMPPTime (s"1990-01-01T00:00:00Z")
	timestamp = Time.formatTime Time.defaultTimeLocale "%s" date
	email = s$"MIME-Version: 1.0\r\n\
	\Message-ID: <" ++ timestamp ++ ".=00.athread@gateway.example.com>\r\n\
	\To: t@example.com\r\n\
	\From: \"f@example.com\" <f=40example=2Ecom@gateway.example.com>\r\n\
	\Jabber-ID: f@example.com\r\n\
	\Date: Mon, 01 Jan 1990 00:00:00 +0000\r\n\
	\Content-Transfer-Encoding: base64\r\n\
	\Content-Disposition: inline\r\n\
	\Content-Type: text/plain; charset=utf-8\r\n\
	\\r\n\
	\5LiW55WMCi4K\r\n"
	message = (XMPP.emptyMessage XMPP.MessageNormal) {
		XMPP.messageTo =
			XMPP.parseJID $ s"t\\40example.com@gateway.example.com",
		XMPP.messageFrom = XMPP.parseJID $ s"f@example.com",
		XMPP.messagePayloads = [
			XML.Element (s"{jabber:component:accept}body")
			[] [XML.NodeContent $ XML.ContentText $ s"世界\n.\n"],
			XML.Element (s"{jabber:component:accept}thread")
			[] [XML.NodeContent $ XML.ContentText $ s"athread"]
		]
	}

unit_messageToEmailWithCheoThread :: IO ()
unit_messageToEmailWithCheoThread =
	fmap (MIME.renderMessage . snd) (
		messageToEmail
		(MIME.DomainLiteral $ encodeUtf8 $ s"gateway.example.com")
		date
		message
	)
	@?=
	Just email
	where
	Just date = parseXMPPTime (s"1990-01-01T00:00:00Z")
	timestamp = Time.formatTime Time.defaultTimeLocale "%s" date
	email = s$"MIME-Version: 1.0\r\n\
	\Message-ID: <" ++ timestamp ++ ".=00.References=3A=20=3Cthing\
		\=40foo=2Etld=3E@gateway.example.com>\r\n\
	\References: <thing@foo.tld>\r\n\
	\To: t@example.com\r\n\
	\From: \"f@example.com\" <f=40example=2Ecom@gateway.example.com>\r\n\
	\Jabber-ID: f@example.com\r\n\
	\Date: Mon, 01 Jan 1990 00:00:00 +0000\r\n\
	\Content-Transfer-Encoding: base64\r\n\
	\Content-Disposition: inline\r\n\
	\Content-Type: text/plain; charset=utf-8\r\n\
	\\r\n\
	\5LiW55WMCi4K\r\n"
	message = (XMPP.emptyMessage XMPP.MessageNormal) {
		XMPP.messageTo =
			XMPP.parseJID $ s"t\\40example.com@gateway.example.com",
		XMPP.messageFrom = XMPP.parseJID $ s"f@example.com",
		XMPP.messagePayloads = [
			XML.Element (s"{jabber:component:accept}body")
			[] [XML.NodeContent $ XML.ContentText $ s"世界\n.\n"],
			XML.Element (s"{jabber:component:accept}thread") [] [
				XML.NodeContent $ XML.ContentText $
				s"References: <thing@foo.tld>"
			]
		]
	}

unit_messageToEmailWithDeepCheoThread :: IO ()
unit_messageToEmailWithDeepCheoThread =
	fmap (MIME.renderMessage . snd) (
		messageToEmail
		(MIME.DomainLiteral $ encodeUtf8 $ s"gateway.example.com")
		date
		message
	)
	@?=
	Just email
	where
	Just date = parseXMPPTime (s"1990-01-01T00:00:00Z")
	timestamp = Time.formatTime Time.defaultTimeLocale "%s" date
	email = s$"MIME-Version: 1.0\r\n\
	\Message-ID: <" ++ timestamp ++ ".=00.References=3A=20=3C1=40t=2Et=3E=\
		\20=3C2=40t=2Et=3E=20=3C3=40t=2Et=3E=20=3C4=40t=2Et=3E=20=3C5=\
		\40t=2Et=3E=20=3C6=40t=2Et=3E@gateway.example.com>\r\n\
	\References: <1@t.t> <3@t.t> <4@t.t> <5@t.t> <6@t.t>\r\n\
	\To: t@example.com\r\n\
	\From: \"f@example.com\" <f=40example=2Ecom@gateway.example.com>\r\n\
	\Jabber-ID: f@example.com\r\n\
	\Date: Mon, 01 Jan 1990 00:00:00 +0000\r\n\
	\Content-Transfer-Encoding: base64\r\n\
	\Content-Disposition: inline\r\n\
	\Content-Type: text/plain; charset=utf-8\r\n\
	\\r\n\
	\5LiW55WMCi4K\r\n"
	message = (XMPP.emptyMessage XMPP.MessageNormal) {
		XMPP.messageTo =
			XMPP.parseJID $ s"t\\40example.com@gateway.example.com",
		XMPP.messageFrom = XMPP.parseJID $ s"f@example.com",
		XMPP.messagePayloads = [
			XML.Element (s"{jabber:component:accept}body")
			[] [XML.NodeContent $ XML.ContentText $ s"世界\n.\n"],
			XML.Element (s"{jabber:component:accept}thread") [] [
				XML.NodeContent $ XML.ContentText $
				s"References: <1@t.t> <2@t.t> <3@t.t> <4@t.t>\
				\ <5@t.t> <6@t.t>"
			]
		]
	}