disable offline messages. postpone prekey handling until after mam catchup

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java  | 196 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java          |   9 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java     |   9 
src/main/java/eu/siacs/conversations/services/MessageArchiveService.java |  13 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |   9 
src/main/java/eu/siacs/conversations/xml/Namespace.java                  |   1 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java            |   4 
7 files changed, 156 insertions(+), 85 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java πŸ”—

@@ -55,6 +55,7 @@ import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
 import eu.siacs.conversations.xmpp.pep.PublishOptions;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 
 public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
@@ -77,11 +78,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	private final Map<Jid, Set<Integer>> deviceIds;
 	private final Map<String, XmppAxolotlMessage> messageCache;
 	private final FetchStatusMap fetchStatusMap;
-	private final HashMap<Jid,List<OnDeviceIdsFetched>> fetchDeviceIdsMap = new HashMap<>();
+	private final HashMap<Jid, List<OnDeviceIdsFetched>> fetchDeviceIdsMap = new HashMap<>();
 	private final SerialSingleThreadExecutor executor;
 	private int numPublishTriesOnEmptyPep = 0;
 	private boolean pepBroken = false;
 	private int lastDeviceListNotificationHash = 0;
+	private Set<XmppAxolotlSession> postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment
 
 	private AtomicBoolean changeAccessMode = new AtomicBoolean(false);
 
@@ -92,12 +94,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 				&& account.getXmppConnection().getFeatures().pep()) {
 			publishBundlesIfNeeded(true, false);
 		} else {
-			Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping OMEMO initialization");
+			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": skipping OMEMO initialization");
 		}
 	}
 
 	public boolean fetchMapHasErrors(List<Jid> jids) {
-		for(Jid jid : jids) {
+		for (Jid jid : jids) {
 			if (deviceIds.get(jid) != null) {
 				for (Integer foreignId : this.deviceIds.get(jid)) {
 					SignalProtocolAddress address = new SignalProtocolAddress(jid.toPreppedString(), foreignId);
@@ -119,7 +121,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	public boolean hasVerifiedKeys(String name) {
-		for(XmppAxolotlSession session : this.sessions.getAll(name).values()) {
+		for (XmppAxolotlSession session : this.sessions.getAll(name).values()) {
 			if (session.getTrust().isVerified()) {
 				return true;
 			}
@@ -194,14 +196,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 			for (Integer deviceId : deviceIds) {
 				SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(bareJid, deviceId);
 				IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey();
-				if(Config.X509_VERIFICATION) {
+				if (Config.X509_VERIFICATION) {
 					X509Certificate certificate = store.getFingerprintCertificate(CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()));
 					if (certificate != null) {
 						Bundle information = CryptoHelper.extractCertificateInformation(certificate);
 						try {
 							final String cn = information.getString("subject_cn");
 							final Jid jid = Jid.fromString(bareJid);
-							Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn);
+							Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn);
 							account.getRoster().getContact(jid).setCommonName(cn);
 						} catch (final InvalidJidException ignored) {
 							//ignored
@@ -215,7 +217,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		private void fillMap(SQLiteAxolotlStore store) {
 			List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toPreppedString());
 			putDevicesForJid(account.getJid().toBareJid().toPreppedString(), deviceIds, store);
-			for (String  address : store.getKnownAddresses()) {
+			for (String address : store.getKnownAddresses()) {
 				deviceIds = store.getSubDeviceSessions(address);
 				putDevicesForJid(address, deviceIds, store);
 			}
@@ -249,9 +251,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 				if (devices == null) {
 					return;
 				}
-				for(Map.Entry<Integer, FetchStatus> entry : devices.entrySet()) {
+				for (Map.Entry<Integer, FetchStatus> entry : devices.entrySet()) {
 					if (entry.getValue() == FetchStatus.ERROR) {
-						Log.d(Config.LOGTAG,"resetting error for "+jid.toBareJid()+"("+entry.getKey()+")");
+						Log.d(Config.LOGTAG, "resetting error for " + jid.toBareJid() + "(" + entry.getKey() + ")");
 						entry.setValue(FetchStatus.TIMEOUT);
 					}
 				}
@@ -294,7 +296,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
 	public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, List<Jid> jids) {
 		Set<IdentityKey> keys = new HashSet<>();
-		for(Jid jid : jids) {
+		for (Jid jid : jids) {
 			keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toPreppedString(), status));
 		}
 		return keys;
@@ -305,7 +307,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) {
-		for(Jid jid : jids) {
+		for (Jid jid : jids) {
 			if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString()) == 0) {
 				return true;
 			}
@@ -325,7 +327,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 
-
 	public Collection<XmppAxolotlSession> findSessionsForContact(Contact contact) {
 		SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid());
 		ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values());
@@ -335,7 +336,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
 	private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
 		HashSet<XmppAxolotlSession> sessions = new HashSet<>();
-		for(Jid jid : conversation.getAcceptedCryptoTargets()) {
+		for (Jid jid : conversation.getAcceptedCryptoTargets()) {
 			sessions.addAll(this.sessions.getAll(getAddressForJid(jid).getName()).values());
 		}
 		return sessions;
@@ -368,13 +369,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	public void destroy() {
-		Log.d(Config.LOGTAG,account.getJid().toBareJid()+": destroying old axolotl service. no longer in use");
+		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": destroying old axolotl service. no longer in use");
 		mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
 	}
 
 	public AxolotlService makeNew() {
-		Log.d(Config.LOGTAG,account.getJid().toBareJid()+": make new axolotl service");
-		return new AxolotlService(this.account,this.mXmppConnectionService);
+		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": make new axolotl service");
+		return new AxolotlService(this.account, this.mXmppConnectionService);
 	}
 
 	public int getOwnDeviceId() {
@@ -382,7 +383,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	public SignalProtocolAddress getOwnAxolotlAddress() {
-		return new SignalProtocolAddress(account.getJid().toBareJid().toPreppedString(),getOwnDeviceId());
+		return new SignalProtocolAddress(account.getJid().toBareJid().toPreppedString(), getOwnDeviceId());
 	}
 
 	public Set<Integer> getOwnDeviceIds() {
@@ -420,7 +421,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 			XmppAxolotlSession session = sessions.get(address);
 			if (session != null && session.getFingerprint() != null) {
 				if (!session.getTrust().isActive()) {
-					Log.d(Config.LOGTAG,"reactivating device with fingerprint "+session.getFingerprint());
+					Log.d(Config.LOGTAG, "reactivating device with fingerprint " + session.getFingerprint());
 					session.setTrust(session.getTrust().toActive());
 				}
 			}
@@ -462,7 +463,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	public void distrustFingerprint(final String fingerprint) {
 		final String fp = fingerprint.replaceAll("\\s", "");
 		final FingerprintStatus fingerprintStatus = axolotlStore.getFingerprintStatus(fp);
-		axolotlStore.setFingerprintStatus(fp,fingerprintStatus.toUntrusted());
+		axolotlStore.setFingerprintStatus(fp, fingerprintStatus.toUntrusted());
 	}
 
 	public void publishOwnDeviceIdIfNeeded() {
@@ -479,8 +480,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 				} else {
 					Element item = mXmppConnectionService.getIqParser().getItem(packet);
 					Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
-					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved own device list: "+deviceIds);
-					registerDevices(account.getJid().toBareJid(),deviceIds);
+					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": retrieved own device list: " + deviceIds);
+					registerDevices(account.getJid().toBareJid(), deviceIds);
 				}
 			}
 		});
@@ -488,18 +489,18 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
 	private Set<Integer> getExpiredDevices() {
 		Set<Integer> devices = new HashSet<>();
-		for(XmppAxolotlSession session : findOwnSessions()) {
+		for (XmppAxolotlSession session : findOwnSessions()) {
 			if (session.getTrust().isActive()) {
 				long diff = System.currentTimeMillis() - session.getTrust().getLastActivation();
 				if (diff > Config.OMEMO_AUTO_EXPIRY) {
-					long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account,session.getFingerprint());
-					long hours = Math.round(lastMessageDiff/(1000*60.0*60.0));
+					long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account, session.getFingerprint());
+					long hours = Math.round(lastMessageDiff / (1000 * 60.0 * 60.0));
 					if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) {
 						devices.add(session.getRemoteAddress().getDeviceId());
 						session.setTrust(session.getTrust().toInactive());
-						Log.d(Config.LOGTAG,account.getJid().toBareJid()+": added own device " + session.getFingerprint() + " to list of expired devices. Last message received "+hours+" hours ago");
+						Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": added own device " + session.getFingerprint() + " to list of expired devices. Last message received " + hours + " hours ago");
 					} else {
-						Log.d(Config.LOGTAG,account.getJid().toBareJid()+": own device "+session.getFingerprint()+" was active "+hours+" hours ago");
+						Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": own device " + session.getFingerprint() + " was active " + hours + " hours ago");
 					}
 				}
 			}
@@ -527,7 +528,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	private void publishDeviceIdsAndRefineAccessModel(Set<Integer> ids) {
-		publishDeviceIdsAndRefineAccessModel(ids,true);
+		publishDeviceIdsAndRefineAccessModel(ids, true);
 	}
 
 	private void publishDeviceIdsAndRefineAccessModel(final Set<Integer> ids, final boolean firstAttempt) {
@@ -537,8 +538,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 			@Override
 			public void onIqPacketReceived(Account account, IqPacket packet) {
 				Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null;
-				if (firstAttempt && error != null && error.hasChild("precondition-not-met",Namespace.PUBSUB_ERROR)) {
-					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": precondition wasn't met for device list. pushing node configuration");
+				if (firstAttempt && error != null && error.hasChild("precondition-not-met", Namespace.PUBSUB_ERROR)) {
+					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": precondition wasn't met for device list. pushing node configuration");
 					mXmppConnectionService.pushNodeConfiguration(account, AxolotlService.PEP_DEVICE_LIST, publishOptions, new XmppConnectionService.OnConfigurationPushed() {
 						@Override
 						public void onPushSucceeded() {
@@ -551,9 +552,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 						}
 					});
 				} else {
-					if (AxolotlService.this.changeAccessMode.compareAndSet(true,false)) {
-						Log.d(Config.LOGTAG,account.getJid().toBareJid()+": done changing access mode");
-						account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE,false);
+					if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) {
+						Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": done changing access mode");
+						account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false);
 						mXmppConnectionService.databaseBackend.updateAccount(account);
 					}
 					if (packet.getType() == IqPacket.TYPE.ERROR) {
@@ -566,39 +567,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord,
-												   final Set<PreKeyRecord> preKeyRecords,
-												   final boolean announceAfter,
-												   final boolean wipe) {
+	                                               final Set<PreKeyRecord> preKeyRecords,
+	                                               final boolean announceAfter,
+	                                               final boolean wipe) {
 		try {
 			IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey();
 			PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias());
 			X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias());
 			Signature verifier = Signature.getInstance("sha256WithRSA");
-			verifier.initSign(x509PrivateKey,mXmppConnectionService.getRNG());
+			verifier.initSign(x509PrivateKey, mXmppConnectionService.getRNG());
 			verifier.update(axolotlPublicKey.serialize());
 			byte[] signature = verifier.sign();
 			IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId());
-			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device "+getOwnDeviceId());
+			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device " + getOwnDeviceId());
 			mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
 				@Override
 				public void onIqPacketReceived(final Account account, IqPacket packet) {
-					String node = AxolotlService.PEP_VERIFICATION+":"+getOwnDeviceId();
+					String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId();
 					mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() {
 						@Override
 						public void onPushSucceeded() {
-							Log.d(Config.LOGTAG,getLogprefix(account) + "configured verification node to be world readable");
+							Log.d(Config.LOGTAG, getLogprefix(account) + "configured verification node to be world readable");
 							publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
 						}
 
 						@Override
 						public void onPushFailed() {
-							Log.d(Config.LOGTAG,getLogprefix(account) + "unable to set access model on verification node");
+							Log.d(Config.LOGTAG, getLogprefix(account) + "unable to set access model on verification node");
 							publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
 						}
 					});
 				}
 			});
-		} catch (Exception  e) {
+		} catch (Exception e) {
 			e.printStackTrace();
 		}
 	}
@@ -612,7 +613,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
 			this.changeAccessMode.set(account.isOptionSet(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE));
 		} else {
-			if (account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE,true)) {
+			if (account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, true)) {
 				Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server doesn’t support publish-options. setting for later access mode change");
 				mXmppConnectionService.databaseBackend.updateAccount(account);
 			}
@@ -728,38 +729,38 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord,
-									 Set<PreKeyRecord> preKeyRecords,
-									 final boolean announceAfter,
-									 final boolean wipe) {
-		publishDeviceBundle(signedPreKeyRecord,preKeyRecords,announceAfter,wipe,true);
+	                                 Set<PreKeyRecord> preKeyRecords,
+	                                 final boolean announceAfter,
+	                                 final boolean wipe) {
+		publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, true);
 	}
 
 	private void publishDeviceBundle(final SignedPreKeyRecord signedPreKeyRecord,
-									 final Set<PreKeyRecord> preKeyRecords,
-									 final boolean announceAfter,
-									 final boolean wipe,
-									 final boolean firstAttempt) {
+	                                 final Set<PreKeyRecord> preKeyRecords,
+	                                 final boolean announceAfter,
+	                                 final boolean wipe,
+	                                 final boolean firstAttempt) {
 		final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null;
 		IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
 				signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
-				preKeyRecords, getOwnDeviceId(),publishOptions);
+				preKeyRecords, getOwnDeviceId(), publishOptions);
 		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing...");
 		mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
 			@Override
 			public void onIqPacketReceived(final Account account, IqPacket packet) {
 				Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null;
 				if (firstAttempt && error != null && error.hasChild("precondition-not-met", Namespace.PUBSUB_ERROR)) {
-					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": precondition wasn't met for bundle. pushing node configuration");
+					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": precondition wasn't met for bundle. pushing node configuration");
 					final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId();
 					mXmppConnectionService.pushNodeConfiguration(account, node, publishOptions, new XmppConnectionService.OnConfigurationPushed() {
 						@Override
 						public void onPushSucceeded() {
-							publishDeviceBundle(signedPreKeyRecord,preKeyRecords, announceAfter, wipe, false);
+							publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false);
 						}
 
 						@Override
 						public void onPushFailed() {
-							publishDeviceBundle(signedPreKeyRecord,preKeyRecords, announceAfter, wipe, false);
+							publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false);
 						}
 					});
 				} else if (packet.getType() == IqPacket.TYPE.RESULT) {
@@ -790,16 +791,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		return conversation.getMode() == Conversation.MODE_SINGLE || (conversation.getMucOptions().nonanonymous() && conversation.getMucOptions().membersOnly());
 	}
 
-	public Pair<AxolotlCapability,Jid> isConversationAxolotlCapableDetailed(Conversation conversation) {
+	public Pair<AxolotlCapability, Jid> isConversationAxolotlCapableDetailed(Conversation conversation) {
 		if (conversation.getMode() == Conversation.MODE_SINGLE
 				|| (conversation.getMucOptions().membersOnly() && conversation.getMucOptions().nonanonymous())) {
 			final List<Jid> jids = getCryptoTargets(conversation);
-			for(Jid jid : jids) {
+			for (Jid jid : jids) {
 				if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) {
 					if (conversation.getAccount().getRoster().getContact(jid).mutualPresenceSubscription()) {
-						return new Pair<>(AxolotlCapability.MISSING_KEYS,jid);
+						return new Pair<>(AxolotlCapability.MISSING_KEYS, jid);
 					} else {
-						return new Pair<>(AxolotlCapability.MISSING_PRESENCE,jid);
+						return new Pair<>(AxolotlCapability.MISSING_PRESENCE, jid);
 					}
 				}
 			}
@@ -845,7 +846,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 			mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
 				@Override
 				public void onIqPacketReceived(Account account, IqPacket packet) {
-					Pair<X509Certificate[],byte[]> verification = mXmppConnectionService.getIqParser().verification(packet);
+					Pair<X509Certificate[], byte[]> verification = mXmppConnectionService.getIqParser().verification(packet);
 					if (verification != null) {
 						try {
 							Signature verifier = Signature.getInstance("sha256WithRSA");
@@ -855,7 +856,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 								try {
 									mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
 									String fingerprint = session.getFingerprint();
-									Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint);
+									Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint);
 									setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true));
 									axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]);
 									fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED);
@@ -863,7 +864,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 									try {
 										final String cn = information.getString("subject_cn");
 										final Jid jid = Jid.fromString(address.getName());
-										Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn);
+										Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn);
 										account.getRoster().getContact(jid).setCommonName(cn);
 									} catch (final InvalidJidException ignored) {
 										//ignored
@@ -871,14 +872,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 									finishBuildingSessionsFromPEP(address);
 									return;
 								} catch (Exception e) {
-									Log.d(Config.LOGTAG,"could not verify certificate");
+									Log.d(Config.LOGTAG, "could not verify certificate");
 								}
 							}
 						} catch (Exception e) {
 							Log.d(Config.LOGTAG, "error during verification " + e.getMessage());
 						}
 					} else {
-						Log.d(Config.LOGTAG,"no verification found");
+						Log.d(Config.LOGTAG, "no verification found");
 					}
 					fetchStatusMap.put(address, FetchStatus.SUCCESS);
 					finishBuildingSessionsFromPEP(address);
@@ -938,7 +939,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	public void fetchDeviceIds(final Jid jid) {
-		fetchDeviceIds(jid,null);
+		fetchDeviceIds(jid, null);
 	}
 
 	public void fetchDeviceIds(final Jid jid, OnDeviceIdsFetched callback) {
@@ -948,14 +949,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 				if (callback != null) {
 					callbacks.add(callback);
 				}
-				Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching device ids for "+jid+" already running. adding callback");
+				Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching device ids for " + jid + " already running. adding callback");
 			} else {
 				callbacks = new ArrayList<>();
 				if (callback != null) {
 					callbacks.add(callback);
 				}
-				this.fetchDeviceIdsMap.put(jid,callbacks);
-				Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching device ids for " + jid);
+				this.fetchDeviceIdsMap.put(jid, callbacks);
+				Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching device ids for " + jid);
 				IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(jid);
 				mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
 					@Override
@@ -967,14 +968,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 								Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
 								registerDevices(jid, deviceIds);
 								if (callbacks != null) {
-									for(OnDeviceIdsFetched callback : callbacks) {
+									for (OnDeviceIdsFetched callback : callbacks) {
 										callback.fetched(jid, deviceIds);
 									}
 								}
 							} else {
 								Log.d(Config.LOGTAG, packet.toString());
 								if (callbacks != null) {
-									for(OnDeviceIdsFetched callback : callbacks) {
+									for (OnDeviceIdsFetched callback : callbacks) {
 										callback.fetched(jid, null);
 									}
 								}
@@ -1086,7 +1087,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
 	public Set<SignalProtocolAddress> findDevicesWithoutSession(final Conversation conversation) {
 		Set<SignalProtocolAddress> addresses = new HashSet<>();
-		for(Jid jid : getCryptoTargets(conversation)) {
+		for (Jid jid : getCryptoTargets(conversation)) {
 			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid);
 			if (deviceIds.get(jid) != null) {
 				for (Integer foreignId : this.deviceIds.get(jid)) {
@@ -1126,7 +1127,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 						if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
 							addresses.add(address);
 						} else {
-							Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken");
+							Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken");
 						}
 					}
 				}
@@ -1138,13 +1139,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
 	public boolean createSessionsIfNeeded(final Conversation conversation) {
 		final List<Jid> jidsWithEmptyDeviceList = getCryptoTargets(conversation);
-		for(Iterator<Jid> iterator = jidsWithEmptyDeviceList.iterator(); iterator.hasNext();) {
+		for (Iterator<Jid> iterator = jidsWithEmptyDeviceList.iterator(); iterator.hasNext(); ) {
 			final Jid jid = iterator.next();
 			if (!hasEmptyDeviceList(jid)) {
 				iterator.remove();
 			}
 		}
-		Log.d(Config.LOGTAG,account.getJid().toBareJid()+": createSessionsIfNeeded() - jids with empty device list: "+jidsWithEmptyDeviceList);
+		Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": createSessionsIfNeeded() - jids with empty device list: " + jidsWithEmptyDeviceList);
 		if (jidsWithEmptyDeviceList.size() > 0) {
 			fetchDeviceIds(jidsWithEmptyDeviceList, new OnMultipleDeviceIdFetched() {
 				@Override
@@ -1183,7 +1184,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		Set<XmppAxolotlSession> sessions = findSessionsForConversation(conversation);
 		sessions.addAll(findOwnSessions());
 		boolean verified = false;
-		for(XmppAxolotlSession session : sessions) {
+		for (XmppAxolotlSession session : sessions) {
 			if (session.getTrust().isTrustedAndActive()) {
 				if (session.getTrust().getTrust() == FingerprintStatus.Trust.VERIFIED_X509) {
 					verified = true;
@@ -1243,7 +1244,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 			Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
 			return null;
 		}
-		if (!buildHeader(axolotlMessage,message.getConversation())) {
+		//TODO: fix this for MUC PMs - Don't encrypt to all participants
+		if (!buildHeader(axolotlMessage, message.getConversation())) {
 			return null;
 		}
 
@@ -1272,7 +1274,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 			@Override
 			public void run() {
 				final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId());
-				if (buildHeader(axolotlMessage,conversation)) {
+				if (buildHeader(axolotlMessage, conversation)) {
 					onMessageCreatedCallback.run(axolotlMessage);
 				} else {
 					onMessageCreatedCallback.run(null);
@@ -1324,7 +1326,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 				postPreKeyMessageHandling(session, preKeyId, postponePreKeyMessageHandling);
 			}
 		} catch (CryptoFailedException e) {
-			Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from "+message.getFrom()+": " + e.getMessage());
+			Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom() + ": " + e.getMessage());
 		}
 
 		if (session.isFresh() && plaintextMessage != null) {
@@ -1335,11 +1337,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	private void postPreKeyMessageHandling(final XmppAxolotlSession session, int preKeyId, final boolean postpone) {
-		Log.d(Config.LOGTAG,account.getJid().toBareJid()+": postPreKeyMessageHandling() preKeyId="+preKeyId+", postpone="+Boolean.toString(postpone));
-		//TODO: do not republish if we already removed this preKeyId
-		publishBundlesIfNeeded(false, false);
+		if (postpone) {
+			postponedSessions.add(session);
+		} else {
+			//TODO: do not republish if we already removed this preKeyId
+			publishBundlesIfNeeded(false, false);
+			completeSession(session);
+		}
+	}
+
+	public void processPostponed() {
+		if (postponedSessions.size() > 0) {
+			publishBundlesIfNeeded(false, false);
+		}
+		Iterator<XmppAxolotlSession> iterator = postponedSessions.iterator();
+		while (iterator.hasNext()) {
+			completeSession(iterator.next());
+			iterator.remove();
+		}
+	}
+
+	private void completeSession(XmppAxolotlSession session) {
+		final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId());
+		axolotlMessage.addDevice(session);
+		try {
+			Jid jid = Jid.fromString(session.getRemoteAddress().getName());
+			MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage);
+			mXmppConnectionService.sendMessagePacket(account, packet);
+		} catch (InvalidJidException e) {
+			throw new Error("Remote addresses are created from jid and should convert back to jid", e);
+		}
 	}
 
+
 	public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) {
 		XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
 
@@ -1351,7 +1381,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 				postPreKeyMessageHandling(session, preKeyId, postponePreKeyMessageHandling);
 			}
 		} catch (CryptoFailedException e) {
-			Log.d(Config.LOGTAG,"could not decrypt keyTransport message "+e.getMessage());
+			Log.d(Config.LOGTAG, "could not decrypt keyTransport message " + e.getMessage());
 			keyTransportMessage = null;
 		}
 
@@ -1363,13 +1393,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	private void putFreshSession(XmppAxolotlSession session) {
-		Log.d(Config.LOGTAG,"put fresh session");
+		Log.d(Config.LOGTAG, "put fresh session");
 		sessions.put(session);
 		if (Config.X509_VERIFICATION) {
 			if (session.getIdentityKey() != null) {
 				verifySessionWithPEP(session);
 			} else {
-				Log.e(Config.LOGTAG,account.getJid().toBareJid()+": identity key was empty after reloading for x509 verification");
+				Log.e(Config.LOGTAG, account.getJid().toBareJid() + ": identity key was empty after reloading for x509 verification");
 			}
 		}
 	}

src/main/java/eu/siacs/conversations/generator/IqGenerator.java πŸ”—

@@ -45,8 +45,7 @@ public class IqGenerator extends AbstractGenerator {
 		final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT);
 		packet.setId(request.getId());
 		packet.setTo(request.getFrom());
-		final Element query = packet.addChild("query",
-				"http://jabber.org/protocol/disco#info");
+		final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info");
 		query.setAttribute("node", request.query().getAttribute("node"));
 		final Element identity = query.addChild("identity");
 		identity.setAttribute("category", "client");
@@ -91,6 +90,12 @@ public class IqGenerator extends AbstractGenerator {
 		return packet;
 	}
 
+	public IqPacket purgeOfflineMessages() {
+		final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+		packet.addChild("offline",Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge");
+		return packet;
+	}
+
 	protected IqPacket publish(final String node, final Element item, final Bundle options) {
 		final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
 		final Element pubsub = packet.addChild("pubsub",

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java πŸ”—

@@ -91,6 +91,15 @@ public class MessageGenerator extends AbstractGenerator {
 		return packet;
 	}
 
+	public MessagePacket generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) {
+		MessagePacket packet = new MessagePacket();
+		packet.setType(MessagePacket.TYPE_CHAT);
+		packet.setTo(to);
+		packet.setAxolotlMessage(axolotlMessage.toElement());
+		packet.addChild("store", "urn:xmpp:hints");
+		return packet;
+	}
+
 	private static boolean recipientSupportsOmemo(Message message) {
 		Contact c = message.getContact();
 		return c != null && c.getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY);

src/main/java/eu/siacs/conversations/services/MessageArchiveService.java πŸ”—

@@ -11,6 +11,7 @@ import java.util.List;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.generator.AbstractGenerator;
@@ -222,6 +223,17 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
 		}
 	}
 
+	public boolean inCatchup(Account account) {
+		synchronized (this.queries) {
+			for(Query query : queries) {
+				if (query.account == account && query.isCatchup() && query.getWith() == null) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+
 	public boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) {
 		synchronized (this.queries) {
 			for(Query query : queries) {
@@ -268,6 +280,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
 			if (query.isCatchup() && query.getActualMessageCount() > 0) {
 				mXmppConnectionService.getNotificationService().finishBacklog(true,query.getAccount());
 			}
+			query.account.getAxolotlService().processPostponed();
 		} else {
 			final Query nextQuery;
 			if (query.getPagingOrder() == PagingOrder.NORMAL) {

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java πŸ”—

@@ -303,6 +303,15 @@ public class XmppConnectionService extends Service {
 			mJingleConnectionManager.cancelInTransmission();
 			fetchRosterFromServer(account);
 			fetchBookmarks(account);
+			final boolean flexible= account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval();
+			final boolean catchup = getMessageArchiveService().inCatchup(account);
+			if (flexible && catchup) {
+				sendIqPacket(account, mIqGenerator.purgeOfflineMessages(), (acc, packet) -> {
+					if (packet.getType() == IqPacket.TYPE.RESULT) {
+						Log.d(Config.LOGTAG, acc.getJid().toBareJid()+": successfully purged offline messages");
+					}
+				});
+			}
 			sendPresence(account);
 			if (mPushManagementService.available(account)) {
 				mPushManagementService.registerPushTokenOnServer(account);

src/main/java/eu/siacs/conversations/xml/Namespace.java πŸ”—

@@ -16,4 +16,5 @@ public final class Namespace {
 	public static final String PUBSUB_PUBLISH_OPTIONS = "http://jabber.org/protocol/pubsub#publish-options";
 	public static final String PUBSUB_ERROR = "http://jabber.org/protocol/pubsub#errors";
 	public static final String NICK = "http://jabber.org/protocol/nick";
+	public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline";
 }

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java πŸ”—

@@ -1681,6 +1681,10 @@ public class XmppConnection implements Runnable {
 			return hasDiscoFeature(account.getServer(), "urn:xmpp:reporting:reason:spam:0");
 		}
 
+		public boolean flexibleOfflineMessageRetrieval() {
+			return hasDiscoFeature(account.getServer(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
+		}
+
 		public boolean register() {
 			return hasDiscoFeature(account.getServer(), Namespace.REGISTER);
 		}