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