MessageParser.java

  1package eu.siacs.conversations.parser;
  2
  3import android.os.Build;
  4import android.text.Html;
  5import android.util.Log;
  6import android.util.Pair;
  7
  8import net.java.otr4j.session.Session;
  9import net.java.otr4j.session.SessionStatus;
 10
 11import java.text.SimpleDateFormat;
 12import java.util.ArrayList;
 13import java.util.Arrays;
 14import java.util.Date;
 15import java.util.List;
 16import java.util.Locale;
 17import java.util.Set;
 18import java.util.UUID;
 19
 20import eu.siacs.conversations.Config;
 21import eu.siacs.conversations.R;
 22import eu.siacs.conversations.crypto.OtrService;
 23import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 24import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 25import eu.siacs.conversations.entities.Account;
 26import eu.siacs.conversations.entities.Bookmark;
 27import eu.siacs.conversations.entities.Contact;
 28import eu.siacs.conversations.entities.Conversation;
 29import eu.siacs.conversations.entities.Message;
 30import eu.siacs.conversations.entities.MucOptions;
 31import eu.siacs.conversations.entities.Presence;
 32import eu.siacs.conversations.entities.ReadByMarker;
 33import eu.siacs.conversations.entities.ServiceDiscoveryResult;
 34import eu.siacs.conversations.http.HttpConnectionManager;
 35import eu.siacs.conversations.services.MessageArchiveService;
 36import eu.siacs.conversations.services.XmppConnectionService;
 37import eu.siacs.conversations.utils.CryptoHelper;
 38import eu.siacs.conversations.xml.Namespace;
 39import eu.siacs.conversations.xml.Element;
 40import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
 41import eu.siacs.conversations.xmpp.chatstate.ChatState;
 42import eu.siacs.conversations.xmpp.jid.Jid;
 43import eu.siacs.conversations.xmpp.pep.Avatar;
 44import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 45
 46public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
 47
 48	private static final List<String> CLIENTS_SENDING_HTML_IN_OTR = Arrays.asList("Pidgin", "Adium", "Trillian");
 49
 50	public MessageParser(XmppConnectionService service) {
 51		super(service);
 52	}
 53
 54	private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) {
 55		ChatState state = ChatState.parse(packet);
 56		if (state != null && c != null) {
 57			final Account account = c.getAccount();
 58			Jid from = packet.getFrom();
 59			if (from.toBareJid().equals(account.getJid().toBareJid())) {
 60				c.setOutgoingChatState(state);
 61				if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
 62					mXmppConnectionService.markRead(c);
 63					activateGracePeriod(account);
 64				}
 65				return false;
 66			} else {
 67				if (isTypeGroupChat) {
 68					MucOptions.User user = c.getMucOptions().findUserByFullJid(from);
 69					if (user != null) {
 70						return user.setChatState(state);
 71					} else {
 72						return false;
 73					}
 74				} else {
 75					return c.setIncomingChatState(state);
 76				}
 77			}
 78		}
 79		return false;
 80	}
 81
 82	private Message parseOtrChat(String body, Jid from, String id, Conversation conversation) {
 83		String presence;
 84		if (from.isBareJid()) {
 85			presence = "";
 86		} else {
 87			presence = from.getResourcepart();
 88		}
 89		if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) {
 90			conversation.endOtrIfNeeded();
 91		}
 92		if (!conversation.hasValidOtrSession()) {
 93			conversation.startOtrSession(presence, false);
 94		} else {
 95			String foreignPresence = conversation.getOtrSession().getSessionID().getUserID();
 96			if (!foreignPresence.equals(presence)) {
 97				conversation.endOtrIfNeeded();
 98				conversation.startOtrSession(presence, false);
 99			}
100		}
101		try {
102			conversation.setLastReceivedOtrMessageId(id);
103			Session otrSession = conversation.getOtrSession();
104			body = otrSession.transformReceiving(body);
105			SessionStatus status = otrSession.getSessionStatus();
106			if (body == null && status == SessionStatus.ENCRYPTED) {
107				mXmppConnectionService.onOtrSessionEstablished(conversation);
108				return null;
109			} else if (body == null && status == SessionStatus.FINISHED) {
110				conversation.resetOtrSession();
111				mXmppConnectionService.updateConversationUi();
112				return null;
113			} else if (body == null || (body.isEmpty())) {
114				return null;
115			}
116			if (body.startsWith(CryptoHelper.FILETRANSFER)) {
117				String key = body.substring(CryptoHelper.FILETRANSFER.length());
118				conversation.setSymmetricKey(CryptoHelper.hexToBytes(key));
119				return null;
120			}
121			if (clientMightSendHtml(conversation.getAccount(), from)) {
122				Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": received OTR message from bad behaving client. escaping HTML…");
123				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
124					body = Html.fromHtml(body, Html.FROM_HTML_MODE_LEGACY).toString();
125				} else {
126					body = Html.fromHtml(body).toString();
127				}
128			}
129
130			final OtrService otrService = conversation.getAccount().getOtrService();
131			Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED);
132			finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey()));
133			conversation.setLastReceivedOtrMessageId(null);
134
135			return finishedMessage;
136		} catch (Exception e) {
137			conversation.resetOtrSession();
138			return null;
139		}
140	}
141
142	private static boolean clientMightSendHtml(Account account, Jid from) {
143		String resource = from.getResourcepart();
144		if (resource == null) {
145			return false;
146		}
147		Presence presence = account.getRoster().getContact(from).getPresences().getPresences().get(resource);
148		ServiceDiscoveryResult disco = presence == null ? null : presence.getServiceDiscoveryResult();
149		if (disco == null) {
150			return false;
151		}
152		return hasIdentityKnowForSendingHtml(disco.getIdentities());
153	}
154
155	private static boolean hasIdentityKnowForSendingHtml(List<ServiceDiscoveryResult.Identity> identities) {
156		for (ServiceDiscoveryResult.Identity identity : identities) {
157			if (identity.getName() != null) {
158				if (CLIENTS_SENDING_HTML_IN_OTR.contains(identity.getName())) {
159					return true;
160				}
161			}
162		}
163		return false;
164	}
165
166	private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, boolean postpone) {
167		final AxolotlService service = conversation.getAccount().getAxolotlService();
168		final XmppAxolotlMessage xmppAxolotlMessage;
169		try {
170			xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid());
171		} catch (Exception e) {
172			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": invalid omemo message received " + e.getMessage());
173			return null;
174		}
175		if (xmppAxolotlMessage.hasPayload()) {
176			final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
177			if (plaintextMessage != null) {
178				Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
179				finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
180				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount()) + " Received Message with session fingerprint: " + plaintextMessage.getFingerprint());
181				return finishedMessage;
182			}
183		} else {
184			Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+": received OMEMO key transport message");
185			service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone);
186		}
187		return null;
188	}
189
190	private class Invite {
191		final Jid jid;
192		final String password;
193		final Contact inviter;
194
195		Invite(Jid jid, String password, Contact inviter) {
196			this.jid = jid;
197			this.password = password;
198			this.inviter = inviter;
199		}
200
201		public boolean execute(Account account) {
202			if (jid != null) {
203				Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
204				if (!conversation.getMucOptions().online()) {
205					conversation.getMucOptions().setPassword(password);
206					mXmppConnectionService.databaseBackend.updateConversation(conversation);
207					mXmppConnectionService.joinMuc(conversation, inviter != null && inviter.mutualPresenceSubscription());
208					mXmppConnectionService.updateConversationUi();
209				}
210				return true;
211			}
212			return false;
213		}
214	}
215
216	private Invite extractInvite(Account account, Element message) {
217		Element x = message.findChild("x", "http://jabber.org/protocol/muc#user");
218		if (x != null) {
219			Element invite = x.findChild("invite");
220			if (invite != null) {
221				Element pw = x.findChild("password");
222				Jid from = invite.getAttributeAsJid("from");
223				Contact contact = from == null ? null : account.getRoster().getContact(from);
224				return new Invite(message.getAttributeAsJid("from"), pw != null ? pw.getContent() : null, contact);
225			}
226		} else {
227			x = message.findChild("x", "jabber:x:conference");
228			if (x != null) {
229				Jid from = message.getAttributeAsJid("from");
230				Contact contact = from == null ? null : account.getRoster().getContact(from);
231				return new Invite(x.getAttributeAsJid("jid"), x.getAttribute("password"), contact);
232			}
233		}
234		return null;
235	}
236
237	private static String extractStanzaId(Element packet, boolean isTypeGroupChat, Conversation conversation) {
238		final Jid by;
239		final boolean safeToExtract;
240		if (isTypeGroupChat) {
241			by = conversation.getJid().toBareJid();
242			safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS);
243		} else {
244			Account account = conversation.getAccount();
245			by = account.getJid().toBareJid();
246			safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
247		}
248		return safeToExtract ? extractStanzaId(packet, by) : null;
249	}
250
251	private static String extractStanzaId(Element packet, Jid by) {
252		for (Element child : packet.getChildren()) {
253			if (child.getName().equals("stanza-id")
254					&& Namespace.STANZA_IDS.equals(child.getNamespace())
255					&& by.equals(child.getAttributeAsJid("by"))) {
256				return child.getAttribute("id");
257			}
258		}
259		return null;
260	}
261
262	private void parseEvent(final Element event, final Jid from, final Account account) {
263		Element items = event.findChild("items");
264		String node = items == null ? null : items.getAttribute("node");
265		if ("urn:xmpp:avatar:metadata".equals(node)) {
266			Avatar avatar = Avatar.parseMetadata(items);
267			if (avatar != null) {
268				avatar.owner = from.toBareJid();
269				if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
270					if (account.getJid().toBareJid().equals(from)) {
271						if (account.setAvatar(avatar.getFilename())) {
272							mXmppConnectionService.databaseBackend.updateAccount(account);
273						}
274						mXmppConnectionService.getAvatarService().clear(account);
275						mXmppConnectionService.updateConversationUi();
276						mXmppConnectionService.updateAccountUi();
277					} else {
278						Contact contact = account.getRoster().getContact(from);
279						contact.setAvatar(avatar);
280						mXmppConnectionService.getAvatarService().clear(contact);
281						mXmppConnectionService.updateConversationUi();
282						mXmppConnectionService.updateRosterUi();
283					}
284				} else if (mXmppConnectionService.isDataSaverDisabled()) {
285					mXmppConnectionService.fetchAvatar(account, avatar);
286				}
287			}
288		} else if ("http://jabber.org/protocol/nick".equals(node)) {
289			final Element i = items.findChild("item");
290			final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK);
291			if (nick != null) {
292				Contact contact = account.getRoster().getContact(from);
293				if (contact.setPresenceName(nick)) {
294					mXmppConnectionService.getAvatarService().clear(contact);
295				}
296				mXmppConnectionService.updateConversationUi();
297				mXmppConnectionService.updateAccountUi();
298			}
299		} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
300			Element item = items.findChild("item");
301			Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
302			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... ");
303			AxolotlService axolotlService = account.getAxolotlService();
304			axolotlService.registerDevices(from, deviceIds);
305			mXmppConnectionService.updateAccountUi();
306		}
307	}
308
309	private boolean handleErrorMessage(Account account, MessagePacket packet) {
310		if (packet.getType() == MessagePacket.TYPE_ERROR) {
311			Jid from = packet.getFrom();
312			if (from != null) {
313				Message message = mXmppConnectionService.markMessage(account,
314						from.toBareJid(),
315						packet.getId(),
316						Message.STATUS_SEND_FAILED,
317						extractErrorMessage(packet));
318				if (message != null) {
319					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
320						message.getConversation().endOtrIfNeeded();
321					}
322				}
323			}
324			return true;
325		}
326		return false;
327	}
328
329	@Override
330	public void onMessagePacketReceived(Account account, MessagePacket original) {
331		if (handleErrorMessage(account, original)) {
332			return;
333		}
334		final MessagePacket packet;
335		Long timestamp = null;
336		final boolean isForwarded;
337		boolean isCarbon = false;
338		String serverMsgId = null;
339		final Element fin = original.findChild("fin", Namespace.MAM_LEGACY);
340		if (fin != null) {
341			mXmppConnectionService.getMessageArchiveService().processFinLegacy(fin, original.getFrom());
342			return;
343		}
344		final boolean mamLegacy = original.hasChild("result", Namespace.MAM_LEGACY);
345		final Element result = original.findChild("result", mamLegacy ? Namespace.MAM_LEGACY : Namespace.MAM);
346		final MessageArchiveService.Query query = result == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(result.getAttribute("queryid"));
347		if (query != null && query.validFrom(original.getFrom())) {
348			Pair<MessagePacket, Long> f = original.getForwardedMessagePacket("result", mamLegacy ? Namespace.MAM_LEGACY : Namespace.MAM);
349			if (f == null) {
350				return;
351			}
352			timestamp = f.second;
353			packet = f.first;
354			isForwarded = true;
355			serverMsgId = result.getAttribute("id");
356			query.incrementMessageCount();
357		} else if (query != null) {
358			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": received mam result from invalid sender");
359			return;
360		} else if (original.fromServer(account)) {
361			Pair<MessagePacket, Long> f;
362			f = original.getForwardedMessagePacket("received", "urn:xmpp:carbons:2");
363			f = f == null ? original.getForwardedMessagePacket("sent", "urn:xmpp:carbons:2") : f;
364			packet = f != null ? f.first : original;
365			if (handleErrorMessage(account, packet)) {
366				return;
367			}
368			timestamp = f != null ? f.second : null;
369			isCarbon = f != null;
370			isForwarded = isCarbon;
371		} else {
372			packet = original;
373			isForwarded = false;
374		}
375
376		if (timestamp == null) {
377			timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
378		}
379		final String body = packet.getBody();
380		final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
381		final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
382		final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
383		final Element oob = packet.findChild("x", Namespace.OOB);
384		final String oobUrl = oob != null ? oob.findChildContent("url") : null;
385		final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
386		final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
387		int status;
388		final Jid counterpart;
389		final Jid to = packet.getTo();
390		final Jid from = packet.getFrom();
391		final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS);
392		final String remoteMsgId;
393		if (originId != null && originId.getAttribute("id") != null) {
394			remoteMsgId = originId.getAttribute("id");
395		} else {
396			remoteMsgId = packet.getId();
397		}
398		boolean notify = false;
399
400		if (from == null) {
401			Log.d(Config.LOGTAG, "no from in: " + packet.toString());
402			return;
403		}
404
405		boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT;
406		if (query != null && !query.muc() && isTypeGroupChat) {
407			Log.e(Config.LOGTAG,account.getJid().toBareJid()+": received groupchat ("+from+") message on regular MAM request. skipping");
408			return;
409		}
410		boolean isProperlyAddressed = (to != null) && (!to.isBareJid() || account.countPresences() == 0);
411		boolean isMucStatusMessage = from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
412		if (packet.fromAccount(account)) {
413			status = Message.STATUS_SEND;
414			counterpart = to != null ? to : account.getJid();
415		} else {
416			status = Message.STATUS_RECEIVED;
417			counterpart = from;
418		}
419
420		Invite invite = extractInvite(account, packet);
421		if (invite != null && invite.execute(account)) {
422			return;
423		}
424
425		if ((body != null || pgpEncrypted != null || axolotlEncrypted != null || oobUrl != null) && !isMucStatusMessage) {
426			final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat, false, query, false);
427			final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
428
429			if (serverMsgId == null) {
430				serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation);
431			}
432
433			if (isTypeGroupChat) {
434				if (conversation.getMucOptions().isSelf(counterpart)) {
435					status = Message.STATUS_SEND_RECEIVED;
436					isCarbon = true; //not really carbon but received from another resource
437					if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId)) {
438						return;
439					} else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
440						Message message = conversation.findSentMessageWithBody(packet.getBody());
441						if (message != null) {
442							mXmppConnectionService.markMessage(message, status);
443							return;
444						}
445					}
446				} else {
447					status = Message.STATUS_RECEIVED;
448				}
449			}
450			final Message message;
451			if (body != null && body.startsWith("?OTR") && Config.supportOtr()) {
452				if (!isForwarded && !isTypeGroupChat && isProperlyAddressed && !conversationMultiMode) {
453					message = parseOtrChat(body, from, remoteMsgId, conversation);
454					if (message == null) {
455						return;
456					}
457				} else {
458					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ignoring OTR message from " + from + " isForwarded=" + Boolean.toString(isForwarded) + ", isProperlyAddressed=" + Boolean.valueOf(isProperlyAddressed));
459					message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
460				}
461			} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
462				message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
463			} else if (axolotlEncrypted != null && Config.supportOmemo()) {
464				Jid origin;
465				if (conversationMultiMode) {
466					final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
467					origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
468					if (origin == null) {
469						Log.d(Config.LOGTAG, "axolotl message in non anonymous conference received");
470						return;
471					}
472				} else {
473					origin = from;
474				}
475				message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status, query != null);
476				if (message == null) {
477					if (query == null &&  extractChatState(mXmppConnectionService.find(account, counterpart.toBareJid()), isTypeGroupChat, packet)) {
478						mXmppConnectionService.updateConversationUi();
479					}
480					if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) {
481						Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId);
482						if (previouslySent != null && previouslySent.getServerMsgId() == null && serverMsgId != null) {
483							previouslySent.setServerMsgId(serverMsgId);
484							mXmppConnectionService.databaseBackend.updateMessage(previouslySent);
485							Log.d(Config.LOGTAG,account.getJid().toBareJid()+": encountered previously sent OMEMO message without serverId. updating...");
486						}
487					}
488					return;
489				}
490				if (conversationMultiMode) {
491					message.setTrueCounterpart(origin);
492				}
493			} else if (body == null && oobUrl != null) {
494				message = new Message(conversation, oobUrl, Message.ENCRYPTION_NONE, status);
495				message.setOob(true);
496				if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
497					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
498				}
499			} else {
500				message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
501			}
502
503			message.setCounterpart(counterpart);
504			message.setRemoteMsgId(remoteMsgId);
505			message.setServerMsgId(serverMsgId);
506			message.setCarbon(isCarbon);
507			message.setTime(timestamp);
508			if (body != null && body.equals(oobUrl)) {
509				message.setOob(true);
510				if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
511					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
512				}
513			}
514			message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
515			if (conversationMultiMode) {
516				final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
517				Jid trueCounterpart;
518				if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
519					trueCounterpart = message.getTrueCounterpart();
520				} else if (query != null && query.safeToExtractTrueCounterpart()) {
521					trueCounterpart = getTrueCounterpart(mucUserElement, fallback);
522				} else {
523					trueCounterpart = fallback;
524				}
525				if (trueCounterpart != null && trueCounterpart.toBareJid().equals(account.getJid().toBareJid())) {
526					status = isTypeGroupChat ? Message.STATUS_SEND_RECEIVED : Message.STATUS_SEND;
527				}
528				message.setStatus(status);
529				message.setTrueCounterpart(trueCounterpart);
530				if (!isTypeGroupChat) {
531					message.setType(Message.TYPE_PRIVATE);
532				}
533			} else {
534				updateLastseen(account, from);
535			}
536
537			if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
538				final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
539						counterpart,
540						message.getStatus() == Message.STATUS_RECEIVED,
541						message.isCarbon());
542				if (replacedMessage != null) {
543					final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
544							|| replacedMessage.getFingerprint().equals(message.getFingerprint());
545					final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
546							&& replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart());
547					final boolean duplicate = conversation.hasDuplicateMessage(message);
548					if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode) && !duplicate) {
549						Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'");
550						synchronized (replacedMessage) {
551							final String uuid = replacedMessage.getUuid();
552							replacedMessage.setUuid(UUID.randomUUID().toString());
553							replacedMessage.setBody(message.getBody());
554							replacedMessage.setEdited(replacedMessage.getRemoteMsgId());
555							replacedMessage.setRemoteMsgId(remoteMsgId);
556							if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
557								replacedMessage.setServerMsgId(message.getServerMsgId());
558							}
559							replacedMessage.setEncryption(message.getEncryption());
560							if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
561								replacedMessage.markUnread();
562							}
563							extractChatState(mXmppConnectionService.find(account, counterpart.toBareJid()), isTypeGroupChat, packet);
564							mXmppConnectionService.updateMessage(replacedMessage, uuid);
565							mXmppConnectionService.getNotificationService().updateNotification(false);
566							if (mXmppConnectionService.confirmMessages()
567									&& (replacedMessage.trusted() || replacedMessage.getType() == Message.TYPE_PRIVATE)
568									&& remoteMsgId != null
569									&& !isForwarded
570									&& !isTypeGroupChat) {
571								sendMessageReceipts(account, packet);
572							}
573							if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
574								conversation.getAccount().getPgpDecryptionService().discard(replacedMessage);
575								conversation.getAccount().getPgpDecryptionService().decrypt(replacedMessage, false);
576							}
577						}
578						return;
579					} else {
580						Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": received message correction but verification didn't check out");
581					}
582				}
583			}
584
585			long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
586			if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
587				Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": skipping message from " + message.getCounterpart().toString() + " because it was sent prior to our deletion date");
588				return;
589			}
590
591			boolean checkForDuplicates = (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
592					|| message.getType() == Message.TYPE_PRIVATE
593					|| message.getServerMsgId() != null;
594			if (checkForDuplicates ) {
595				final Message duplicate = conversation.findDuplicateMessage(message);
596				if (duplicate != null) {
597					final boolean serverMsgIdUpdated;
598					if (duplicate.getStatus() != Message.STATUS_RECEIVED
599							&& duplicate.getUuid().equals(message.getRemoteMsgId())
600							&& duplicate.getServerMsgId() == null
601							&& message.getServerMsgId() != null) {
602						duplicate.setServerMsgId(message.getServerMsgId());
603						mXmppConnectionService.databaseBackend.updateMessage(message);
604						serverMsgIdUpdated = true;
605					} else {
606						serverMsgIdUpdated = false;
607					}
608					Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart()+". serverMsgIdUpdated="+Boolean.toString(serverMsgIdUpdated));
609					return;
610				}
611			}
612
613			if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
614				conversation.prepend(message);
615			} else {
616				conversation.add(message);
617			}
618			if (query != null) {
619				query.incrementActualMessageCount();
620			}
621
622			if (query == null || query.isCatchup()) { //either no mam or catchup
623				if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
624					mXmppConnectionService.markRead(conversation);
625					if (query == null) {
626						activateGracePeriod(account);
627					}
628				} else {
629					message.markUnread();
630					notify = true;
631				}
632			}
633
634			if (message.getEncryption() == Message.ENCRYPTION_PGP) {
635				notify = conversation.getAccount().getPgpDecryptionService().decrypt(message, notify);
636			}
637
638			if (query == null) {
639				extractChatState(mXmppConnectionService.find(account, counterpart.toBareJid()), isTypeGroupChat, packet);
640				mXmppConnectionService.updateConversationUi();
641			}
642
643			if (mXmppConnectionService.confirmMessages()
644					&& (message.trusted() || message.getType() == Message.TYPE_PRIVATE)
645					&& remoteMsgId != null
646					&& !isForwarded
647					&& !isTypeGroupChat) {
648				sendMessageReceipts(account, packet);
649			}
650
651			if (message.getStatus() == Message.STATUS_RECEIVED
652					&& conversation.getOtrSession() != null
653					&& !conversation.getOtrSession().getSessionID().getUserID()
654					.equals(message.getCounterpart().getResourcepart())) {
655				conversation.endOtrIfNeeded();
656			}
657
658			mXmppConnectionService.databaseBackend.createMessage(message);
659			final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
660			if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
661				manager.createNewDownloadConnection(message);
662			} else if (notify) {
663				if (query != null && query.isCatchup()) {
664					mXmppConnectionService.getNotificationService().pushFromBacklog(message);
665				} else {
666					mXmppConnectionService.getNotificationService().push(message);
667				}
668			}
669		} else if (!packet.hasChild("body")) { //no body
670
671			if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.toBareJid()), isTypeGroupChat, packet)) {
672				mXmppConnectionService.updateConversationUi();
673			}
674
675			final Conversation conversation = mXmppConnectionService.find(account, from.toBareJid());
676			if (isTypeGroupChat) {
677				if (packet.hasChild("subject")) {
678					if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
679						conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
680						String subject = packet.findChildContent("subject");
681						if (conversation.getMucOptions().setSubject(subject)) {
682							mXmppConnectionService.updateConversation(conversation);
683						}
684						final Bookmark bookmark = conversation.getBookmark();
685						if (bookmark != null && bookmark.getBookmarkName() == null) {
686							if (bookmark.setBookmarkName(subject)) {
687								mXmppConnectionService.pushBookmarks(account);
688							}
689						}
690						mXmppConnectionService.updateConversationUi();
691						return;
692					}
693				}
694			}
695			if (conversation != null && mucUserElement != null && from.isBareJid()) {
696				for (Element child : mucUserElement.getChildren()) {
697					if ("status".equals(child.getName())) {
698						try {
699							int code = Integer.parseInt(child.getAttribute("code"));
700							if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
701								mXmppConnectionService.fetchConferenceConfiguration(conversation);
702								break;
703							}
704						} catch (Exception e) {
705							//ignored
706						}
707					} else if ("item".equals(child.getName())) {
708						MucOptions.User user = AbstractParser.parseItem(conversation, child);
709						Log.d(Config.LOGTAG, account.getJid() + ": changing affiliation for "
710								+ user.getRealJid() + " to " + user.getAffiliation() + " in "
711								+ conversation.getJid().toBareJid());
712						if (!user.realJidMatchesAccount()) {
713							boolean isNew = conversation.getMucOptions().updateUser(user);
714							mXmppConnectionService.getAvatarService().clear(conversation);
715							mXmppConnectionService.updateMucRosterUi();
716							mXmppConnectionService.updateConversationUi();
717							if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
718								Jid jid = user.getRealJid();
719								List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
720								if (cryptoTargets.remove(user.getRealJid())) {
721									Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName());
722									conversation.setAcceptedCryptoTargets(cryptoTargets);
723									mXmppConnectionService.updateConversation(conversation);
724								}
725							} else if (isNew && user.getRealJid() != null && account.getAxolotlService().hasEmptyDeviceList(user.getRealJid())) {
726								account.getAxolotlService().fetchDeviceIds(user.getRealJid());
727							}
728						}
729					}
730				}
731			}
732		}
733
734		Element received = packet.findChild("received", "urn:xmpp:chat-markers:0");
735		if (received == null) {
736			received = packet.findChild("received", "urn:xmpp:receipts");
737		}
738		if (received != null && !packet.fromAccount(account)) {
739			mXmppConnectionService.markMessage(account, from.toBareJid(), received.getAttribute("id"), Message.STATUS_SEND_RECEIVED);
740		}
741		Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0");
742		if (displayed != null) {
743			final String id = displayed.getAttribute("id");
744			final Jid sender = displayed.getAttributeAsJid("sender");
745			if (packet.fromAccount(account)) {
746				Conversation conversation = mXmppConnectionService.find(account, counterpart.toBareJid());
747				if (conversation != null && (query == null || query.isCatchup())) {
748					mXmppConnectionService.markRead(conversation); //TODO only mark messages read that are older than timestamp
749				}
750			} else if (isTypeGroupChat) {
751				Conversation conversation = mXmppConnectionService.find(account, counterpart.toBareJid());
752				if (conversation != null && id != null && sender != null) {
753					Message message = conversation.findMessageWithRemoteId(id, sender);
754					if (message != null) {
755						final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
756						final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback);
757						final boolean trueJidMatchesAccount = account.getJid().toBareJid().equals(trueJid == null ? null : trueJid.toBareJid());
758						if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
759							if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections
760								mXmppConnectionService.markRead(conversation);
761							}
762						} else  if (!counterpart.isBareJid() && trueJid != null){
763							ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
764							if (message.addReadByMarker(readByMarker)) {
765								Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": added read by (" + readByMarker.getRealJid() + ") to message '" + message.getBody() + "'");
766								mXmppConnectionService.updateMessage(message);
767							}
768						}
769					}
770				}
771			} else {
772				final Message displayedMessage = mXmppConnectionService.markMessage(account, from.toBareJid(), id, Message.STATUS_SEND_DISPLAYED);
773				Message message = displayedMessage == null ? null : displayedMessage.prev();
774				while (message != null
775						&& message.getStatus() == Message.STATUS_SEND_RECEIVED
776						&& message.getTimeSent() < displayedMessage.getTimeSent()) {
777					mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED);
778					message = message.prev();
779				}
780			}
781		}
782
783		Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
784		if (event != null) {
785			parseEvent(event, original.getFrom(), account);
786		}
787
788		final String nick = packet.findChildContent("nick", Namespace.NICK);
789		if (nick != null) {
790			Contact contact = account.getRoster().getContact(from);
791			if (contact.setPresenceName(nick)) {
792				mXmppConnectionService.getAvatarService().clear(contact);
793			}
794		}
795	}
796
797	private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
798		final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
799		Jid result = item == null ? null : item.getAttributeAsJid("jid");
800		return result != null ? result : fallback;
801	}
802
803	private void sendMessageReceipts(Account account, MessagePacket packet) {
804		ArrayList<String> receiptsNamespaces = new ArrayList<>();
805		if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
806			receiptsNamespaces.add("urn:xmpp:chat-markers:0");
807		}
808		if (packet.hasChild("request", "urn:xmpp:receipts")) {
809			receiptsNamespaces.add("urn:xmpp:receipts");
810		}
811		if (receiptsNamespaces.size() > 0) {
812			MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
813					packet,
814					receiptsNamespaces,
815					packet.getType());
816			mXmppConnectionService.sendMessagePacket(account, receipt);
817		}
818	}
819
820	private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
821
822	private void activateGracePeriod(Account account) {
823		long duration = mXmppConnectionService.getLongPreference("grace_period_length", R.integer.grace_period) * 1000;
824		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": activating grace period till " + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
825		account.activateGracePeriod(duration);
826	}
827}