Message.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5
  6import java.net.MalformedURLException;
  7import java.net.URL;
  8import java.util.Arrays;
  9
 10import eu.siacs.conversations.Config;
 11import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
 12import eu.siacs.conversations.utils.GeoHelper;
 13import eu.siacs.conversations.utils.MimeUtils;
 14import eu.siacs.conversations.utils.UIHelper;
 15import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 16import eu.siacs.conversations.xmpp.jid.Jid;
 17
 18public class Message extends AbstractEntity {
 19
 20	public static final String TABLENAME = "messages";
 21
 22	public static final String MERGE_SEPARATOR = " \u200B\n\n";
 23
 24	public static final int STATUS_RECEIVED = 0;
 25	public static final int STATUS_UNSEND = 1;
 26	public static final int STATUS_SEND = 2;
 27	public static final int STATUS_SEND_FAILED = 3;
 28	public static final int STATUS_WAITING = 5;
 29	public static final int STATUS_OFFERED = 6;
 30	public static final int STATUS_SEND_RECEIVED = 7;
 31	public static final int STATUS_SEND_DISPLAYED = 8;
 32
 33	public static final int ENCRYPTION_NONE = 0;
 34	public static final int ENCRYPTION_PGP = 1;
 35	public static final int ENCRYPTION_OTR = 2;
 36	public static final int ENCRYPTION_DECRYPTED = 3;
 37	public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
 38	public static final int ENCRYPTION_AXOLOTL = 5;
 39
 40	public static final int TYPE_TEXT = 0;
 41	public static final int TYPE_IMAGE = 1;
 42	public static final int TYPE_FILE = 2;
 43	public static final int TYPE_STATUS = 3;
 44	public static final int TYPE_PRIVATE = 4;
 45
 46	public static final String CONVERSATION = "conversationUuid";
 47	public static final String COUNTERPART = "counterpart";
 48	public static final String TRUE_COUNTERPART = "trueCounterpart";
 49	public static final String BODY = "body";
 50	public static final String TIME_SENT = "timeSent";
 51	public static final String ENCRYPTION = "encryption";
 52	public static final String STATUS = "status";
 53	public static final String TYPE = "type";
 54	public static final String CARBON = "carbon";
 55	public static final String OOB = "oob";
 56	public static final String EDITED = "edited";
 57	public static final String REMOTE_MSG_ID = "remoteMsgId";
 58	public static final String SERVER_MSG_ID = "serverMsgId";
 59	public static final String RELATIVE_FILE_PATH = "relativeFilePath";
 60	public static final String FINGERPRINT = "axolotl_fingerprint";
 61	public static final String READ = "read";
 62	public static final String ME_COMMAND = "/me ";
 63
 64
 65	public boolean markable = false;
 66	protected String conversationUuid;
 67	protected Jid counterpart;
 68	protected Jid trueCounterpart;
 69	protected String body;
 70	protected String encryptedBody;
 71	protected long timeSent;
 72	protected int encryption;
 73	protected int status;
 74	protected int type;
 75	protected boolean carbon = false;
 76	protected boolean oob = false;
 77	protected String edited = null;
 78	protected String relativeFilePath;
 79	protected boolean read = true;
 80	protected String remoteMsgId = null;
 81	protected String serverMsgId = null;
 82	protected Conversation conversation = null;
 83	protected Transferable transferable = null;
 84	private Message mNextMessage = null;
 85	private Message mPreviousMessage = null;
 86	private String axolotlFingerprint = null;
 87
 88	private Message() {
 89
 90	}
 91
 92	public Message(Conversation conversation, String body, int encryption) {
 93		this(conversation, body, encryption, STATUS_UNSEND);
 94	}
 95
 96	public Message(Conversation conversation, String body, int encryption, int status) {
 97		this(java.util.UUID.randomUUID().toString(),
 98				conversation.getUuid(),
 99				conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
100				null,
101				body,
102				System.currentTimeMillis(),
103				encryption,
104				status,
105				TYPE_TEXT,
106				false,
107				null,
108				null,
109				null,
110				null,
111				true,
112				null,
113				false);
114		this.conversation = conversation;
115	}
116
117	private Message(final String uuid, final String conversationUUid, final Jid counterpart,
118					final Jid trueCounterpart, final String body, final long timeSent,
119					final int encryption, final int status, final int type, final boolean carbon,
120					final String remoteMsgId, final String relativeFilePath,
121					final String serverMsgId, final String fingerprint, final boolean read,
122					final String edited, final boolean oob) {
123		this.uuid = uuid;
124		this.conversationUuid = conversationUUid;
125		this.counterpart = counterpart;
126		this.trueCounterpart = trueCounterpart;
127		this.body = body;
128		this.timeSent = timeSent;
129		this.encryption = encryption;
130		this.status = status;
131		this.type = type;
132		this.carbon = carbon;
133		this.remoteMsgId = remoteMsgId;
134		this.relativeFilePath = relativeFilePath;
135		this.serverMsgId = serverMsgId;
136		this.axolotlFingerprint = fingerprint;
137		this.read = read;
138		this.edited = edited;
139		this.oob = oob;
140	}
141
142	public static Message fromCursor(Cursor cursor) {
143		Jid jid;
144		try {
145			String value = cursor.getString(cursor.getColumnIndex(COUNTERPART));
146			if (value != null) {
147				jid = Jid.fromString(value, true);
148			} else {
149				jid = null;
150			}
151		} catch (InvalidJidException e) {
152			jid = null;
153		}
154		Jid trueCounterpart;
155		try {
156			String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART));
157			if (value != null) {
158				trueCounterpart = Jid.fromString(value, true);
159			} else {
160				trueCounterpart = null;
161			}
162		} catch (InvalidJidException e) {
163			trueCounterpart = null;
164		}
165		return new Message(cursor.getString(cursor.getColumnIndex(UUID)),
166				cursor.getString(cursor.getColumnIndex(CONVERSATION)),
167				jid,
168				trueCounterpart,
169				cursor.getString(cursor.getColumnIndex(BODY)),
170				cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
171				cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
172				cursor.getInt(cursor.getColumnIndex(STATUS)),
173				cursor.getInt(cursor.getColumnIndex(TYPE)),
174				cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
175				cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
176				cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
177				cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
178				cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
179				cursor.getInt(cursor.getColumnIndex(READ)) > 0,
180				cursor.getString(cursor.getColumnIndex(EDITED)),
181				cursor.getInt(cursor.getColumnIndex(OOB)) > 0);
182	}
183
184	public static Message createStatusMessage(Conversation conversation, String body) {
185		final Message message = new Message();
186		message.setType(Message.TYPE_STATUS);
187		message.setConversation(conversation);
188		message.setBody(body);
189		return message;
190	}
191
192	public static Message createLoadMoreMessage(Conversation conversation) {
193		final Message message = new Message();
194		message.setType(Message.TYPE_STATUS);
195		message.setConversation(conversation);
196		message.setBody("LOAD_MORE");
197		return message;
198	}
199
200	@Override
201	public ContentValues getContentValues() {
202		ContentValues values = new ContentValues();
203		values.put(UUID, uuid);
204		values.put(CONVERSATION, conversationUuid);
205		if (counterpart == null) {
206			values.putNull(COUNTERPART);
207		} else {
208			values.put(COUNTERPART, counterpart.toString());
209		}
210		if (trueCounterpart == null) {
211			values.putNull(TRUE_COUNTERPART);
212		} else {
213			values.put(TRUE_COUNTERPART, trueCounterpart.toString());
214		}
215		values.put(BODY, body);
216		values.put(TIME_SENT, timeSent);
217		values.put(ENCRYPTION, encryption);
218		values.put(STATUS, status);
219		values.put(TYPE, type);
220		values.put(CARBON, carbon ? 1 : 0);
221		values.put(REMOTE_MSG_ID, remoteMsgId);
222		values.put(RELATIVE_FILE_PATH, relativeFilePath);
223		values.put(SERVER_MSG_ID, serverMsgId);
224		values.put(FINGERPRINT, axolotlFingerprint);
225		values.put(READ,read ? 1 : 0);
226		values.put(EDITED, edited);
227		values.put(OOB, oob ? 1 : 0);
228		return values;
229	}
230
231	public String getConversationUuid() {
232		return conversationUuid;
233	}
234
235	public Conversation getConversation() {
236		return this.conversation;
237	}
238
239	public void setConversation(Conversation conv) {
240		this.conversation = conv;
241	}
242
243	public Jid getCounterpart() {
244		return counterpart;
245	}
246
247	public void setCounterpart(final Jid counterpart) {
248		this.counterpart = counterpart;
249	}
250
251	public Contact getContact() {
252		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
253			return this.conversation.getContact();
254		} else {
255			if (this.trueCounterpart == null) {
256				return null;
257			} else {
258				return this.conversation.getAccount().getRoster()
259						.getContactFromRoster(this.trueCounterpart);
260			}
261		}
262	}
263
264	public String getBody() {
265		return body;
266	}
267
268	public void setBody(String body) {
269		this.body = body;
270	}
271
272	public long getTimeSent() {
273		return timeSent;
274	}
275
276	public int getEncryption() {
277		return encryption;
278	}
279
280	public void setEncryption(int encryption) {
281		this.encryption = encryption;
282	}
283
284	public int getStatus() {
285		return status;
286	}
287
288	public void setStatus(int status) {
289		this.status = status;
290	}
291
292	public String getRelativeFilePath() {
293		return this.relativeFilePath;
294	}
295
296	public void setRelativeFilePath(String path) {
297		this.relativeFilePath = path;
298	}
299
300	public String getRemoteMsgId() {
301		return this.remoteMsgId;
302	}
303
304	public void setRemoteMsgId(String id) {
305		this.remoteMsgId = id;
306	}
307
308	public String getServerMsgId() {
309		return this.serverMsgId;
310	}
311
312	public void setServerMsgId(String id) {
313		this.serverMsgId = id;
314	}
315
316	public boolean isRead() {
317		return this.read;
318	}
319
320	public void markRead() {
321		this.read = true;
322	}
323
324	public void markUnread() {
325		this.read = false;
326	}
327
328	public void setTime(long time) {
329		this.timeSent = time;
330	}
331
332	public String getEncryptedBody() {
333		return this.encryptedBody;
334	}
335
336	public void setEncryptedBody(String body) {
337		this.encryptedBody = body;
338	}
339
340	public int getType() {
341		return this.type;
342	}
343
344	public void setType(int type) {
345		this.type = type;
346	}
347
348	public boolean isCarbon() {
349		return carbon;
350	}
351
352	public void setCarbon(boolean carbon) {
353		this.carbon = carbon;
354	}
355
356	public void setEdited(String edited) {
357		this.edited = edited;
358	}
359
360	public boolean edited() {
361		return this.edited != null;
362	}
363
364	public void setTrueCounterpart(Jid trueCounterpart) {
365		this.trueCounterpart = trueCounterpart;
366	}
367
368	public Jid getTrueCounterpart() {
369		return this.trueCounterpart;
370	}
371
372	public Transferable getTransferable() {
373		return this.transferable;
374	}
375
376	public void setTransferable(Transferable transferable) {
377		this.transferable = transferable;
378	}
379
380	public boolean equals(Message message) {
381		if (this.serverMsgId != null && message.getServerMsgId() != null) {
382			return this.serverMsgId.equals(message.getServerMsgId());
383		} else if (this.body == null || this.counterpart == null) {
384			return false;
385		} else {
386			String body, otherBody;
387			if (this.hasFileOnRemoteHost()) {
388				body = getFileParams().url.toString();
389				otherBody = message.body == null ? null : message.body.trim();
390			} else {
391				body = this.body;
392				otherBody = message.body;
393			}
394			if (message.getRemoteMsgId() != null) {
395				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
396						&& this.counterpart.equals(message.getCounterpart())
397						&& (body.equals(otherBody)
398						||(message.getEncryption() == Message.ENCRYPTION_PGP
399						&&  message.getRemoteMsgId().matches("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"))) ;
400			} else {
401				return this.remoteMsgId == null
402						&& this.counterpart.equals(message.getCounterpart())
403						&& body.equals(otherBody)
404						&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
405			}
406		}
407	}
408
409	public Message next() {
410		synchronized (this.conversation.messages) {
411			if (this.mNextMessage == null) {
412				int index = this.conversation.messages.indexOf(this);
413				if (index < 0 || index >= this.conversation.messages.size() - 1) {
414					this.mNextMessage = null;
415				} else {
416					this.mNextMessage = this.conversation.messages.get(index + 1);
417				}
418			}
419			return this.mNextMessage;
420		}
421	}
422
423	public Message prev() {
424		synchronized (this.conversation.messages) {
425			if (this.mPreviousMessage == null) {
426				int index = this.conversation.messages.indexOf(this);
427				if (index <= 0 || index > this.conversation.messages.size()) {
428					this.mPreviousMessage = null;
429				} else {
430					this.mPreviousMessage = this.conversation.messages.get(index - 1);
431				}
432			}
433			return this.mPreviousMessage;
434		}
435	}
436
437	public boolean isLastCorrectableMessage() {
438		Message next = next();
439		while(next != null) {
440			if (next.isCorrectable()) {
441				return false;
442			}
443			next = next.next();
444		}
445		return isCorrectable();
446	}
447
448	private boolean isCorrectable() {
449		return getStatus() != STATUS_RECEIVED && !isCarbon();
450	}
451
452	public boolean mergeable(final Message message) {
453		return message != null &&
454				(message.getType() == Message.TYPE_TEXT &&
455						this.getTransferable() == null &&
456						message.getTransferable() == null &&
457						message.getEncryption() != Message.ENCRYPTION_PGP &&
458						message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
459						this.getType() == message.getType() &&
460						//this.getStatus() == message.getStatus() &&
461						isStatusMergeable(this.getStatus(), message.getStatus()) &&
462						this.getEncryption() == message.getEncryption() &&
463						this.getCounterpart() != null &&
464						this.getCounterpart().equals(message.getCounterpart()) &&
465						this.edited() == message.edited() &&
466						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
467						!GeoHelper.isGeoUri(message.getBody()) &&
468						!GeoHelper.isGeoUri(this.body) &&
469						message.treatAsDownloadable() == Decision.NEVER &&
470						this.treatAsDownloadable() == Decision.NEVER &&
471						!message.getBody().startsWith(ME_COMMAND) &&
472						!this.getBody().startsWith(ME_COMMAND) &&
473						!this.bodyIsHeart() &&
474						!message.bodyIsHeart() &&
475						this.isTrusted() == message.isTrusted()
476				);
477	}
478
479	private static boolean isStatusMergeable(int a, int b) {
480		return a == b || (
481				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
482						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
483						|| (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND)
484						|| (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED)
485						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
486						|| (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED)
487		);
488	}
489
490	public String getMergedBody() {
491		StringBuilder body = new StringBuilder(this.body.trim());
492		Message current = this;
493		while(current.mergeable(current.next())) {
494			current = current.next();
495			body.append(MERGE_SEPARATOR);
496			body.append(current.getBody().trim());
497		}
498		return body.toString();
499	}
500
501	public boolean hasMeCommand() {
502		return getMergedBody().startsWith(ME_COMMAND);
503	}
504
505	public int getMergedStatus() {
506		int status = this.status;
507		Message current = this;
508		while(current.mergeable(current.next())) {
509			current = current.next();
510			status = current.status;
511		}
512		return status;
513	}
514
515	public long getMergedTimeSent() {
516		long time = this.timeSent;
517		Message current = this;
518		while(current.mergeable(current.next())) {
519			current = current.next();
520			time = current.timeSent;
521		}
522		return time;
523	}
524
525	public boolean wasMergedIntoPrevious() {
526		Message prev = this.prev();
527		return prev != null && prev.mergeable(this);
528	}
529
530	public boolean trusted() {
531		Contact contact = this.getContact();
532		return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
533	}
534
535	public boolean fixCounterpart() {
536		Presences presences = conversation.getContact().getPresences();
537		if (counterpart != null && presences.has(counterpart.getResourcepart())) {
538			return true;
539		} else if (presences.size() >= 1) {
540			try {
541				counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
542						conversation.getJid().getDomainpart(),
543						presences.asStringArray()[0]);
544				return true;
545			} catch (InvalidJidException e) {
546				counterpart = null;
547				return false;
548			}
549		} else {
550			counterpart = null;
551			return false;
552		}
553	}
554
555	public void setUuid(String uuid) {
556		this.uuid = uuid;
557	}
558
559	public String getEditedId() {
560		return edited;
561	}
562
563	public void setOob(boolean isOob) {
564		this.oob = isOob;
565	}
566
567	public enum Decision {
568		MUST,
569		SHOULD,
570		NEVER,
571	}
572
573	private static String extractRelevantExtension(URL url) {
574		String path = url.getPath();
575		return extractRelevantExtension(path);
576	}
577
578	private static String extractRelevantExtension(String path) {
579		if (path == null || path.isEmpty()) {
580			return null;
581		}
582		
583		String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
584		int dotPosition = filename.lastIndexOf(".");
585
586		if (dotPosition != -1) {
587			String extension = filename.substring(dotPosition + 1);
588			// we want the real file extension, not the crypto one
589			if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
590				return extractRelevantExtension(filename.substring(0,dotPosition));
591			} else {
592				return extension;
593			}
594		}
595		return null;
596	}
597
598	public String getMimeType() {
599		if (relativeFilePath != null) {
600			int start = relativeFilePath.lastIndexOf('.') + 1;
601			if (start < relativeFilePath.length()) {
602				return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
603			} else {
604				return null;
605			}
606		} else {
607			try {
608				return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
609			} catch (MalformedURLException e) {
610				return null;
611			}
612		}
613	}
614
615	public Decision treatAsDownloadable() {
616		if (body.trim().contains(" ")) {
617			return Decision.NEVER;
618		}
619		try {
620			URL url = new URL(body);
621			if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
622				return Decision.NEVER;
623			} else if (oob) {
624				return Decision.MUST;
625			}
626			String extension = extractRelevantExtension(url);
627			if (extension == null) {
628				return Decision.NEVER;
629			}
630			String ref = url.getRef();
631			boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
632
633			if (encrypted) {
634				return Decision.MUST;
635			} else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension)
636					|| Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) {
637				return Decision.SHOULD;
638			} else {
639				return Decision.NEVER;
640			}
641
642		} catch (MalformedURLException e) {
643			return Decision.NEVER;
644		}
645	}
646
647	public boolean bodyIsHeart() {
648		return body != null && UIHelper.HEARTS.contains(body.trim());
649	}
650
651	public FileParams getFileParams() {
652		FileParams params = getLegacyFileParams();
653		if (params != null) {
654			return params;
655		}
656		params = new FileParams();
657		if (this.transferable != null) {
658			params.size = this.transferable.getFileSize();
659		}
660		if (body == null) {
661			return params;
662		}
663		String parts[] = body.split("\\|");
664		switch (parts.length) {
665			case 1:
666				try {
667					params.size = Long.parseLong(parts[0]);
668				} catch (NumberFormatException e) {
669					try {
670						params.url = new URL(parts[0]);
671					} catch (MalformedURLException e1) {
672						params.url = null;
673					}
674				}
675				break;
676			case 2:
677			case 4:
678				try {
679					params.url = new URL(parts[0]);
680				} catch (MalformedURLException e1) {
681					params.url = null;
682				}
683				try {
684					params.size = Long.parseLong(parts[1]);
685				} catch (NumberFormatException e) {
686					params.size = 0;
687				}
688				try {
689					params.width = Integer.parseInt(parts[2]);
690				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
691					params.width = 0;
692				}
693				try {
694					params.height = Integer.parseInt(parts[3]);
695				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
696					params.height = 0;
697				}
698				break;
699			case 3:
700				try {
701					params.size = Long.parseLong(parts[0]);
702				} catch (NumberFormatException e) {
703					params.size = 0;
704				}
705				try {
706					params.width = Integer.parseInt(parts[1]);
707				} catch (NumberFormatException e) {
708					params.width = 0;
709				}
710				try {
711					params.height = Integer.parseInt(parts[2]);
712				} catch (NumberFormatException e) {
713					params.height = 0;
714				}
715				break;
716		}
717		return params;
718	}
719
720	public FileParams getLegacyFileParams() {
721		FileParams params = new FileParams();
722		if (body == null) {
723			return params;
724		}
725		String parts[] = body.split(",");
726		if (parts.length == 3) {
727			try {
728				params.size = Long.parseLong(parts[0]);
729			} catch (NumberFormatException e) {
730				return null;
731			}
732			try {
733				params.width = Integer.parseInt(parts[1]);
734			} catch (NumberFormatException e) {
735				return null;
736			}
737			try {
738				params.height = Integer.parseInt(parts[2]);
739			} catch (NumberFormatException e) {
740				return null;
741			}
742			return params;
743		} else {
744			return null;
745		}
746	}
747
748	public void untie() {
749		this.mNextMessage = null;
750		this.mPreviousMessage = null;
751	}
752
753	public boolean isFileOrImage() {
754		return type == TYPE_FILE || type == TYPE_IMAGE;
755	}
756
757	public boolean hasFileOnRemoteHost() {
758		return isFileOrImage() && getFileParams().url != null;
759	}
760
761	public boolean needsUploading() {
762		return isFileOrImage() && getFileParams().url == null;
763	}
764
765	public class FileParams {
766		public URL url;
767		public long size = 0;
768		public int width = 0;
769		public int height = 0;
770	}
771
772	public void setFingerprint(String fingerprint) {
773		this.axolotlFingerprint = fingerprint;
774	}
775
776	public String getFingerprint() {
777		return axolotlFingerprint;
778	}
779
780	public boolean isTrusted() {
781		XmppAxolotlSession.Trust t = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
782		return t != null && t.trusted();
783	}
784
785	private  int getPreviousEncryption() {
786		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
787			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
788				continue;
789			}
790			return iterator.getEncryption();
791		}
792		return ENCRYPTION_NONE;
793	}
794
795	private int getNextEncryption() {
796		for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
797			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
798				continue;
799			}
800			return iterator.getEncryption();
801		}
802		return conversation.getNextEncryption();
803	}
804
805	public boolean isValidInSession() {
806		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
807		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
808
809		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
810				|| futureEncryption == ENCRYPTION_NONE
811				|| pastEncryption != futureEncryption;
812
813		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
814	}
815
816	private static int getCleanedEncryption(int encryption) {
817		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
818			return ENCRYPTION_PGP;
819		}
820		return encryption;
821	}
822}