Index: source/lobby/XmppClient.cpp
===================================================================
--- source/lobby/XmppClient.cpp	(revision 17900)
+++ source/lobby/XmppClient.cpp	(working copy)
@@ -592,8 +592,7 @@
 		scriptInterface.SetProperty(ret, "level", message.level);
 	if (!message.data.empty())
 		scriptInterface.SetProperty(ret, "data", message.data);
-	if (!message.datetime.empty())
-		scriptInterface.SetProperty(ret, "datetime", message.datetime);
+	scriptInterface.SetProperty(ret, "time", message.time);
 
 	m_GuiMessageQueue.pop_front();
 }
@@ -655,9 +654,7 @@
 	message.level = L"room-message";
 	message.from = wstring_from_utf8(msg.from().resource().to_string());
 	message.text = wstring_from_utf8(msg.body().to_string());
-	if (msg.when())
-		// See http://xmpp.org/extensions/xep-0082.html#sect-idp285136 for format
-		message.datetime = msg.when()->stamp().to_string();
+	message.time = ComputeTimestamp(msg);
 	PushGuiMessage(message);
 }
 
@@ -674,9 +671,7 @@
 	message.level = L"private-message";
 	message.from = wstring_from_utf8(msg.from().username().to_string());
 	message.text = wstring_from_utf8(msg.body().to_string());
-	if (msg.when())
-		//See http://xmpp.org/extensions/xep-0082.html#sect-idp285136 for format
-		message.datetime = msg.when()->stamp().to_string();
+	message.time = ComputeTimestamp(msg);
 	PushGuiMessage(message);
 }
 
@@ -769,6 +764,7 @@
 	message.level = wstring_from_utf8(level);
 	message.text = wstring_from_utf8(text);
 	message.data = wstring_from_utf8(data);
+	message.time = time(NULL);
 	PushGuiMessage(message);
 }
 
@@ -939,6 +935,26 @@
  *****************************************************/
 
 /**
+ * Compute the POSIX timestamp of a message. Uses message datetime when possible, current time otherwise.
+ *
+ * @param msg The message on which to base the computation
+ * @returns POSIX GMT/UTC seconds since Jan. 1st 1970
+ */
+time_t XmppClient::ComputeTimestamp(const glooxwrapper::Message& msg) {
+	if (!msg.when())
+		return time(NULL);
+
+	glooxwrapper::string timestampStr = msg.when()->stamp();
+	struct tm timestamp = {0};
+	// See http://xmpp.org/extensions/xep-0082.html#sect-idp285136 for format
+	void * res = strptime(timestampStr.c_str(), "%Y-%m-%dT%H:%M:%SZ", &timestamp);
+	if (res == NULL)
+		LOGERROR("Recived delayed message with corrupted timestamp %s", timestampStr.to_string());
+	// mktime returns localtime, so we have to re-adjust back to GMT/UTC
+	return mktime(&timestamp) - timezone;
+}
+
+/**
  * Convert a gloox presence type to string.
  *
  * @param p Presence to be converted
Index: source/lobby/XmppClient.h
===================================================================
--- source/lobby/XmppClient.h	(revision 17900)
+++ source/lobby/XmppClient.h	(working copy)
@@ -127,6 +127,7 @@
 	std::string StanzaErrorToString(gloox::StanzaError err) const;
 	std::string ConnectionErrorToString(gloox::ConnectionError err) const;
 	std::string RegistrationResultToString(gloox::RegistrationResult res) const;
+	time_t ComputeTimestamp(const glooxwrapper::Message& msg);
 
 public:
 	/* Messages */
@@ -138,13 +139,13 @@
 		std::wstring data;
 		std::wstring from;
 		std::wstring message;
-		std::string datetime;
+		time_t time;
 	};
 	void GuiPollMessage(ScriptInterface& scriptInterface, JS::MutableHandleValue ret);
 	void SendMUCMessage(const std::string& message);
 	void ClearPresenceUpdates();
 	int GetMucMessageCount();
-	protected:
+protected:
 	void PushGuiMessage(XmppClient::GUIMessage message);
 	void CreateGUIMessage(const std::string& type, const std::string& level, const std::string& text = "", const std::string& data = "");
 
Index: binaries/data/mods/public/gui/lobby/lobby.js
===================================================================
--- binaries/data/mods/public/gui/lobby/lobby.js	(revision 17900)
+++ binaries/data/mods/public/gui/lobby/lobby.js	(working copy)
@@ -111,10 +111,15 @@
 			updateLeaderboard();
 			updatePlayerList();
 			Engine.GetGUIObjectByName("hostButton").enabled = false;
-			addChatMessage({ "from": "system", "text": translate("Disconnected.") + msg.text, "color": g_SystemColor });
+			addChatMessage({
+				"from": "system",
+				"text": translate("Disconnected.") + msg.text,
+				"color": g_SystemColor,
+				"time": msg.time
+			});
 		},
 		"error": msg => {
-			addChatMessage({ "from": "system", "text": msg.text, "color": g_SystemColor });
+			addChatMessage({ "from": "system", "text": msg.text, "color": g_SystemColor, "time": msg.time});
 		}
 	},
 	"chat": {
@@ -124,7 +129,8 @@
 		"join": msg => {
 			addChatMessage({
 				"text": "/special " + sprintf(translate("%(nick)s has joined."), { "nick": msg.text }),
-				"isSpecial": true
+				"isSpecial": true,
+				"time": msg.time
 			});
 			Engine.SendGetRatingList();
 		},
@@ -131,7 +137,8 @@
 		"leave": msg => {
 			addChatMessage({
 				"text": "/special " + sprintf(translate("%(nick)s has left."), { "nick": msg.text }),
-				"isSpecial": true
+				"isSpecial": true,
+				"time": msg.time
 			});
 		},
 		"presence": msg => {
@@ -142,7 +149,8 @@
 					"oldnick": msg.text,
 					"newnick": msg.data
 				}),
-				"isSpecial": true
+				"isSpecial": true,
+				"time": msg.time
 			});
 		},
 		"room-message": msg => {
@@ -149,7 +157,7 @@
 			addChatMessage({
 				"from": escapeText(msg.from),
 				"text": escapeText(msg.text),
-				"datetime": msg.datetime
+				"time": msg.time
 			});
 		},
 		"private-message": msg => {
@@ -157,7 +165,7 @@
 				addChatMessage({
 					"from": "(Private) " + escapeText(msg.from), // TODO: placeholder
 					"text": escapeText(msg.text.trim()), // some XMPP clients send trailing whitespace
-					"datetime": msg.datetime
+					"time": msg.time
 				});
 		}
 	},
@@ -656,7 +664,8 @@
 			"text": sprintf(
 				translate("This game's address '%(ip)s' does not appear to be valid."),
 				{ "ip": game.ip }
-			)
+			),
+			"time": Date.now() / 1000
 		});
 		return;
 	}
@@ -767,7 +776,8 @@
 	default:
 		addChatMessage({
 			"from": "system",
-			"text": sprintf(translate("We're sorry, the '%(cmd)s' command is not supported."), { "cmd": cmd })
+			"text": sprintf(translate("We're sorry, the '%(cmd)s' command is not supported."), { "cmd": cmd }),
+			"time": Date.now() / 1000
 		});
 	}
 	return true;
@@ -789,13 +799,9 @@
 		if (g_Username != msg.from)
 			msg.text = msg.text.replace(g_Username, colorPlayerName(g_Username));
 
-		// Run spam test if it's not a historical message
-		if (!msg.datetime)
-		{
-			updateSpamMonitor(msg.from);
-			if (isSpam(msg.text, msg.from))
-				return;
-		}
+		updateSpamMonitor(msg);
+		if (isSpam(msg.text, msg.from))
+			return;
 	}
 
 	var formatted = ircFormat(msg);
@@ -877,23 +883,10 @@
 	if (!g_ShowTimestamp)
 		return formattedMessage;
 
-	var time;
-	if (msg.datetime)
-	{
-		let dTime = msg.datetime.split("T");
-		let parserDate = dTime[0].split("-");
-		let parserTime = dTime[1].split(":");
-		// See http://xmpp.org/extensions/xep-0082.html#sect-idp285136 for format of datetime
-		// Date takes Year, Month, Day, Hour, Minute, Second
-		time = new Date(Date.UTC(parserDate[0], parserDate[1], parserDate[2], parserTime[0], parserTime[1], parserTime[2].split("Z")[0]));
-	}
-	else
-		time = new Date(Date.now());
-
 	// Translation: Time as shown in the multiplayer lobby (when you enable it in the options page).
 	// For a list of symbols that you can use, see:
 	// https://sites.google.com/site/icuprojectuserguide/formatparse/datetime?pli=1#TOC-Date-Field-Symbol-Table
-	var timeString = Engine.FormatMillisecondsIntoDateString(time.getTime(), translate("HH:mm"));
+	var timeString = Engine.FormatMillisecondsIntoDateString(msg.time * 1000, translate("HH:mm"));
 
 	// Translation: Time prefix as shown in the multiplayer lobby (when you enable it in the options page).
 	var timePrefixString = '[font="' + g_SenderFont + '"]' + sprintf(translate("\\[%(time)s]"), { "time": timeString }) + '[/font]';
@@ -905,14 +898,19 @@
 /**
  * Update the spam monitor.
  *
- * @param {string} from - User to update.
+ * @param {Object} msg - Message containing user to update.
  */
-function updateSpamMonitor(from)
+function updateSpamMonitor(msg)
 {
-	if (g_SpamMonitor[from])
-		++g_SpamMonitor[from][0];
+	// Ignore historical messages (older than 5s)
+	// TODO: Properly check for spam in historical messages as well
+	if (msg.time < Date.now() / 1000 - 5)
+		return;
+
+	if (g_SpamMonitor[msg.from])
+		++g_SpamMonitor[msg.from][0];
 	else
-		g_SpamMonitor[from] = [1, Math.floor(Date.now() / 1000), 0];
+		g_SpamMonitor[msg.from] = [1, Math.floor(Date.now() / 1000), 0];
 }
 
 /**
@@ -945,7 +943,11 @@
 	{
 		g_SpamMonitor[from][2] = time;
 		if (from == g_Username)
-			addChatMessage({ "from": "system", "text": translate("Please do not spam. You have been blocked for thirty seconds.") });
+			addChatMessage({
+				"from": "system",
+				"text": translate("Please do not spam. You have been blocked for thirty seconds."),
+				"time": Date.now() / 1000
+			});
 		return true;
 	}
 
