MessageParser.java

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