/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.impl.protocol.jabber;
import java.util.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.carbon.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.mailnotification.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.messagecorrection.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.Message;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.protocol.jabberconstants.*;
import net.java.sip.communicator.util.*;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smack.provider.*;
import org.jivesoftware.smack.util.*;
import org.jivesoftware.smackx.*;
import org.jivesoftware.smackx.packet.*;
/**
* A straightforward implementation of the basic instant messaging operation
* set.
*
* @author Damian Minkov
* @author Matthieu Helleringer
* @author Alain Knaebel
* @author Emil Ivov
* @author Hristo Terezov
*/
public class OperationSetBasicInstantMessagingJabberImpl
extends AbstractOperationSetBasicInstantMessaging
implements OperationSetMessageCorrection
{
/**
* Our class logger
*/
private static final Logger logger =
Logger.getLogger(OperationSetBasicInstantMessagingJabberImpl.class);
/**
* The maximum number of unread threads that we'd be notifying the user of.
*/
private static final String PNAME_MAX_GMAIL_THREADS_PER_NOTIFICATION
= "net.java.sip.communicator.impl.protocol.jabber."
+"MAX_GMAIL_THREADS_PER_NOTIFICATION";
/**
* A table mapping contact addresses to full jids that can be used to
* target a specific resource (rather than sending a message to all logged
* instances of a user).
*/
private Map<String, StoredThreadID> jids
= new Hashtable<String, StoredThreadID>();
/**
* The most recent full JID used for the contact address.
*/
private Map<String, String> recentJIDForAddress
= new Hashtable<String, String>();
/**
* The smackMessageListener instance listens for incoming messages.
* Keep a reference of it so if anything goes wrong we don't add
* two different instances.
*/
private SmackMessageListener smackMessageListener = null;
/**
* Contains the complete jid of a specific user and the time that it was
* last used so that we could remove it after a certain point.
*/
public static class StoredThreadID
{
/** The time that we last sent or received a message from this jid */
long lastUpdatedTime;
/** The last chat used, this way we will reuse the thread-id */
String threadID;
}
/**
* A prefix helps to make sure that thread ID's are unique across mutliple
* instances.
*/
private static String prefix = StringUtils.randomString(5);
/**
* Keeps track of the current increment, which is appended to the prefix to
* forum a unique thread ID.
*/
private static long id = 0;
/**
* The number of milliseconds that we preserve threads with no traffic
* before considering them dead.
*/
private static final long JID_INACTIVITY_TIMEOUT = 10*60*1000;//10 min.
/**
* Indicates the time of the last Mailbox report that we received from
* Google (if this is a Google server we are talking to). Should be included
* in all following mailbox queries
*/
private long lastReceivedMailboxResultTime = -1;
/**
* The provider that created us.
*/
private final ProtocolProviderServiceJabberImpl jabberProvider;
/**
* A reference to the persistent presence operation set that we use
* to match incoming messages to <tt>Contact</tt>s and vice versa.
*/
private OperationSetPersistentPresenceJabberImpl opSetPersPresence = null;
/**
* The opening BODY HTML TAG: <body>
*/
private static final String OPEN_BODY_TAG = "<body>";
/**
* The closing BODY HTML TAG: <body>
*/
private static final String CLOSE_BODY_TAG = "</body>";
/**
* The html namespace used as feature
* XHTMLManager.namespace
*/
private final static String HTML_NAMESPACE =
"http://jabber.org/protocol/xhtml-im";
/**
* List of filters to be used to filter which messages to handle
* current Operation Set.
*/
private List<PacketFilter> packetFilters = new ArrayList<PacketFilter>();
/**
* Whether carbon is enabled or not.
*/
private boolean isCarbonEnabled = false;
/**
* Creates an instance of this operation set.
* @param provider a reference to the <tt>ProtocolProviderServiceImpl</tt>
* that created us and that we'll use for retrieving the underlying aim
* connection.
*/
OperationSetBasicInstantMessagingJabberImpl(
ProtocolProviderServiceJabberImpl provider)
{
this.jabberProvider = provider;
packetFilters.add(new GroupMessagePacketFilter());
packetFilters.add(
new PacketTypeFilter(org.jivesoftware.smack.packet.Message.class));
provider.addRegistrationStateChangeListener(
new RegistrationStateListener());
ProviderManager man = ProviderManager.getInstance();
MessageCorrectionExtensionProvider extProvider =
new MessageCorrectionExtensionProvider();
man.addExtensionProvider(MessageCorrectionExtension.ELEMENT_NAME,
MessageCorrectionExtension.NAMESPACE,
extProvider);
}
/**
* Create a Message instance with the specified UID, content type
* and a default encoding.
* This method can be useful when message correction is required. One can
* construct the corrected message to have the same UID as the message
* before correction.
*
* @param messageText the string content of the message.
* @param contentType the MIME-type for <tt>content</tt>
* @param messageUID the unique identifier of this message.
* @return Message the newly created message
*/
public Message createMessageWithUID(
String messageText, String contentType, String messageUID)
{
return new MessageJabberImpl(messageText, contentType,
DEFAULT_MIME_ENCODING, null, messageUID);
}
/**
* Create a Message instance for sending arbitrary MIME-encoding content.
*
* @param content content value
* @param contentType the MIME-type for <tt>content</tt>
* @return the newly created message.
*/
public Message createMessage(String content, String contentType)
{
return createMessage(content, contentType, DEFAULT_MIME_ENCODING, null);
}
/**
* Create a Message instance for sending arbitrary MIME-encoding content.
*
* @param content content value
* @param contentType the MIME-type for <tt>content</tt>
* @param subject the Subject of the message that we'd like to create.
* @param encoding the enconding of the message that we will be sending.
*
* @return the newly created message.
*/
@Override
public Message createMessage(String content, String contentType,
String encoding, String subject)
{
return new MessageJabberImpl(content, contentType, encoding, subject);
}
Message createMessage(String content, String contentType,
String messageUID)
{
return new MessageJabberImpl(content, contentType,
DEFAULT_MIME_ENCODING, null, messageUID);
}
/**
* Determines wheter the protocol provider (or the protocol itself) support
* sending and receiving offline messages. Most often this method would
* return true for protocols that support offline messages and false for
* those that don't. It is however possible for a protocol to support these
* messages and yet have a particular account that does not (i.e. feature
* not enabled on the protocol server). In cases like this it is possible
* for this method to return true even when offline messaging is not
* supported, and then have the sendMessage method throw an
* OperationFailedException with code - OFFLINE_MESSAGES_NOT_SUPPORTED.
*
* @return <tt>true</tt> if the protocol supports offline messages and
* <tt>false</tt> otherwise.
*/
public boolean isOfflineMessagingSupported()
{
return true;
}
/**
* Determines wheter the protocol supports the supplied content type
*
* @param contentType the type we want to check
* @return <tt>true</tt> if the protocol supports it and
* <tt>false</tt> otherwise.
*/
public boolean isContentTypeSupported(String contentType)
{
return
(contentType.equals(DEFAULT_MIME_TYPE)
|| contentType.equals(HTML_MIME_TYPE));
}
/**
* Determines whether the protocol supports the supplied content type
* for the given contact.
*
* @param contentType the type we want to check
* @param contact contact which is checked for supported contentType
* @return <tt>true</tt> if the contact supports it and
* <tt>false</tt> otherwise.
*/
@Override
public boolean isContentTypeSupported(String contentType, Contact contact)
{
// by default we support default mime type, for other mimetypes
// method must be overriden
if(contentType.equals(DEFAULT_MIME_TYPE))
return true;
else if(contentType.equals(HTML_MIME_TYPE))
{
String toJID = recentJIDForAddress.get(contact.getAddress());
if (toJID == null)
toJID = contact.getAddress();
return jabberProvider.isFeatureListSupported(
toJID,
HTML_NAMESPACE);
}
return false;
}
/**
* Remove from our <tt>jids</tt> map all entries that have not seen any
* activity (i.e. neither outgoing nor incoming messags) for more than
* JID_INACTIVITY_TIMEOUT. Note that this method is not synchronous and that
* it is only meant for use by the {@link #getThreadIDForAddress(String)} and
* {@link #putJidForAddress(String, String)}
*/
private void purgeOldJids()
{
long currentTime = System.currentTimeMillis();
Iterator<Map.Entry<String, StoredThreadID>> entries
= jids.entrySet().iterator();
while( entries.hasNext() )
{
Map.Entry<String, StoredThreadID> entry = entries.next();
StoredThreadID target = entry.getValue();
if (currentTime - target.lastUpdatedTime
> JID_INACTIVITY_TIMEOUT)
entries.remove();
}
}
/**
* Returns the last jid that the party with the specified <tt>address</tt>
* contacted us from or <tt>null</tt>(or bare jid) if we don't have a jid
* for the specified <tt>address</tt> yet. The method would also purge all
* entries that haven't seen any activity (i.e. no one has tried to get or
* remap it) for a delay longer than <tt>JID_INACTIVITY_TIMEOUT</tt>.
*
* @param jid the <tt>jid</tt> that we'd like to obtain a threadID for.
*
* @return the last jid that the party with the specified <tt>address</tt>
* contacted us from or <tt>null</tt> if we don't have a jid for the
* specified <tt>address</tt> yet.
*/
String getThreadIDForAddress(String jid)
{
synchronized(jids)
{
purgeOldJids();
StoredThreadID ta = jids.get(jid);
if (ta == null)
return null;
ta.lastUpdatedTime = System.currentTimeMillis();
return ta.threadID;
}
}
/**
* Maps the specified <tt>address</tt> to <tt>jid</tt>. The point of this
* method is to allow us to send all messages destined to the contact with
* the specified <tt>address</tt> to the <tt>jid</tt> that they last
* contacted us from.
*
* @param threadID the threadID of conversation.
* @param jid the jid (i.e. address/resource) that the contact with the
* specified <tt>address</tt> last contacted us from.
*/
private void putJidForAddress(String jid, String threadID)
{
synchronized(jids)
{
purgeOldJids();
StoredThreadID ta = jids.get(jid);
if (ta == null)
{
ta = new StoredThreadID();
jids.put(jid, ta);
}
recentJIDForAddress.put(StringUtils.parseBareAddress(jid), jid);
ta.lastUpdatedTime = System.currentTimeMillis();
ta.threadID = threadID;
}
}
/**
* Helper function used to send a message to a contact, with the given
* extensions attached.
*
* @param to The contact to send the message to.
* @param toResource The resource to send the message to or null if no
* resource has been specified
* @param message The message to send.
* @param extensions The XMPP extensions that should be attached to the
* message before sending.
* @return The MessageDeliveryEvent that resulted after attempting to
* send this message, so the calling function can modify it if needed.
*/
private MessageDeliveredEvent sendMessage( Contact to,
ContactResource toResource,
Message message,
PacketExtension[] extensions)
{
if( !(to instanceof ContactJabberImpl) )
throw new IllegalArgumentException(
"The specified contact is not a Jabber contact."
+ to);
assertConnected();
org.jivesoftware.smack.packet.Message msg =
new org.jivesoftware.smack.packet.Message();
String toJID = null;
if (toResource != null)
{
if(toResource.equals(ContactResource.BASE_RESOURCE))
{
toJID = to.getAddress();
}
else
toJID =
((ContactResourceJabberImpl) toResource).getFullJid();
}
if (toJID == null)
{
toJID = to.getAddress();
}
msg.setPacketID(message.getMessageUID());
msg.setTo(toJID);
for (PacketExtension ext : extensions)
{
msg.addExtension(ext);
}
if (logger.isTraceEnabled())
logger.trace("Will send a message to:" + toJID
+ " chat.jid=" + toJID);
MessageDeliveredEvent msgDeliveryPendingEvt
= new MessageDeliveredEvent(message, to, toResource);
MessageDeliveredEvent[] transformedEvents = messageDeliveryPendingTransform(msgDeliveryPendingEvt);
if (transformedEvents == null || transformedEvents.length == 0)
return null;
for (MessageDeliveredEvent event : transformedEvents)
{
String content = event.getSourceMessage().getContent();
if (message.getContentType().equals(HTML_MIME_TYPE))
{
msg.setBody(Html2Text.extractText(content));
// Check if the other user supports XHTML messages
// make sure we use our discovery manager as it caches calls
if (jabberProvider
.isFeatureListSupported(toJID, HTML_NAMESPACE))
{
// Add the XHTML text to the message
XHTMLManager.addBody(msg, OPEN_BODY_TAG + content
+ CLOSE_BODY_TAG);
}
}
else
{
// this is plain text so keep it as it is.
msg.setBody(content);
}
// msg.addExtension(new Version());
if (event.isMessageEncrypted() && isCarbonEnabled)
{
msg.addExtension(new CarbonPacketExtension.PrivateExtension());
}
MessageEventManager.addNotificationsRequests(msg, true, false,
false, true);
String threadID = getThreadIDForAddress(toJID);
if (threadID == null)
threadID = nextThreadID();
msg.setThread(threadID);
msg.setType(org.jivesoftware.smack.packet.Message.Type.chat);
msg.setFrom(jabberProvider.getConnection().getUser());
jabberProvider.getConnection().sendPacket(msg);
putJidForAddress(toJID, threadID);
}
return new MessageDeliveredEvent(message, to, toResource);
}
/**
* Sends the <tt>message</tt> to the destination indicated by the
* <tt>to</tt> contact.
*
* @param to the <tt>Contact</tt> to send <tt>message</tt> to
* @param message the <tt>Message</tt> to send.
* @throws java.lang.IllegalStateException if the underlying stack is
* not registered and initialized.
* @throws java.lang.IllegalArgumentException if <tt>to</tt> is not an
* instance of ContactImpl.
*/
public void sendInstantMessage(Contact to, Message message)
throws IllegalStateException, IllegalArgumentException
{
sendInstantMessage(to, null, message);
}
/**
* Sends the <tt>message</tt> to the destination indicated by the
* <tt>to</tt>. Provides a default implementation of this method.
*
* @param to the <tt>Contact</tt> to send <tt>message</tt> to
* @param toResource the resource to which the message should be send
* @param message the <tt>Message</tt> to send.
* @throws java.lang.IllegalStateException if the underlying ICQ stack is
* not registered and initialized.
* @throws java.lang.IllegalArgumentException if <tt>to</tt> is not an
* instance belonging to the underlying implementation.
*/
@Override
public void sendInstantMessage( Contact to,
ContactResource toResource,
Message message)
throws IllegalStateException,
IllegalArgumentException
{
MessageDeliveredEvent msgDelivered =
sendMessage(to, toResource, message, new PacketExtension[0]);
fireMessageEvent(msgDelivered);
}
/**
* Replaces the message with ID <tt>correctedMessageUID</tt> sent to
* the contact <tt>to</tt> with the message <tt>message</tt>
*
* @param to The contact to send the message to.
* @param message The new message.
* @param correctedMessageUID The ID of the message being replaced.
*/
public void correctMessage(
Contact to, ContactResource resource,
Message message, String correctedMessageUID)
{
PacketExtension[] exts = new PacketExtension[1];
exts[0] = new MessageCorrectionExtension(correctedMessageUID);
MessageDeliveredEvent msgDelivered
= sendMessage(to, resource, message, exts);
msgDelivered.setCorrectedMessageUID(correctedMessageUID);
fireMessageEvent(msgDelivered);
}
/**
* Utility method throwing an exception if the stack is not properly
* initialized.
*
* @throws java.lang.IllegalStateException if the underlying stack is
* not registered and initialized.
*/
private void assertConnected()
throws IllegalStateException
{
if (opSetPersPresence == null)
{
throw
new IllegalStateException(
"The provider must be signed on the service before"
+ " being able to communicate.");
}
else
opSetPersPresence.assertConnected();
}
/**
* Our listener that will tell us when we're registered to
*/
private class RegistrationStateListener
implements RegistrationStateChangeListener
{
/**
* The method is called by a ProtocolProvider implementation whenever
* a change in the registration state of the corresponding provider had
* occurred.
* @param evt ProviderStatusChangeEvent the event describing the status
* change.
*/
public void registrationStateChanged(RegistrationStateChangeEvent evt)
{
if (logger.isDebugEnabled())
logger.debug("The provider changed state from: "
+ evt.getOldState()
+ " to: " + evt.getNewState());
if (evt.getNewState() == RegistrationState.REGISTERING)
{
opSetPersPresence
= (OperationSetPersistentPresenceJabberImpl)
jabberProvider.getOperationSet(
OperationSetPersistentPresence.class);
if(smackMessageListener == null)
{
smackMessageListener = new SmackMessageListener();
}
else
{
// make sure this listener is not already installed in this
// connection
jabberProvider.getConnection()
.removePacketListener(smackMessageListener);
}
jabberProvider.getConnection().addPacketListener(
smackMessageListener,
new AndFilter(
packetFilters.toArray(
new PacketFilter[packetFilters.size()])));
}
else if (evt.getNewState() == RegistrationState.REGISTERED)
{
new Thread(new Runnable()
{
@Override
public void run()
{
initAdditionalServices();
}
}).start();
}
else if(evt.getNewState() == RegistrationState.UNREGISTERED
|| evt.getNewState() == RegistrationState.CONNECTION_FAILED
|| evt.getNewState() == RegistrationState.AUTHENTICATION_FAILED)
{
if(jabberProvider.getConnection() != null)
{
if(smackMessageListener != null)
jabberProvider.getConnection().removePacketListener(
smackMessageListener);
}
smackMessageListener = null;
}
}
}
/**
* Initialize additional services, like gmail notifications and message
* carbons.
*/
private void initAdditionalServices()
{
//subscribe for Google (Gmail or Google Apps) notifications
//for new mail messages.
boolean enableGmailNotifications
= jabberProvider
.getAccountID()
.getAccountPropertyBoolean(
"GMAIL_NOTIFICATIONS_ENABLED",
false);
if (enableGmailNotifications)
subscribeForGmailNotifications();
boolean enableCarbon
= isCarbonSupported() && !jabberProvider.getAccountID()
.getAccountPropertyBoolean(
ProtocolProviderFactory.IS_CARBON_DISABLED,
false);
if(enableCarbon)
{
enableDisableCarbon(true);
}
else
{
isCarbonEnabled = false;
}
}
/**
* Sends enable or disable carbon packet to the server.
* @param enable if <tt>true</tt> sends enable packet otherwise sends
* disable packet.
*/
private void enableDisableCarbon(final boolean enable)
{
IQ iq = new IQ(){
@Override
public String getChildElementXML()
{
return "<" + (enable? "enable" : "disable") + " xmlns='urn:xmpp:carbons:2' />";
}
};
Packet response = null;
try
{
PacketCollector packetCollector
= jabberProvider.getConnection().createPacketCollector(
new PacketIDFilter(iq.getPacketID()));
iq.setFrom(jabberProvider.getOurJID());
iq.setType(IQ.Type.SET);
jabberProvider.getConnection().sendPacket(iq);
response
= packetCollector.nextResult(
SmackConfiguration.getPacketReplyTimeout());
packetCollector.cancel();
}
catch(Exception e)
{
logger.error("Failed to enable carbon.", e);
}
isCarbonEnabled = false;
if (response == null)
{
logger.error(
"Failed to enable carbon. No response is received.");
}
else if (response.getError() != null)
{
logger.error(
"Failed to enable carbon: "
+ response.getError());
}
else if (!(response instanceof IQ)
|| !((IQ) response).getType().equals(IQ.Type.RESULT))
{
logger.error(
"Failed to enable carbon. The response is not correct.");
}
else
{
isCarbonEnabled = true;
}
}
/**
* Checks whether the carbon is supported by the server or not.
* @return <tt>true</tt> if carbon is supported by the server and
* <tt>false</tt> if not.
*/
private boolean isCarbonSupported()
{
try
{
return jabberProvider.getDiscoveryManager().discoverInfo(
jabberProvider.getAccountID().getService())
.containsFeature(CarbonPacketExtension.NAMESPACE);
}
catch (XMPPException e)
{
logger.warn("Failed to retrieve carbon support." + e.getMessage());
}
return false;
}
/**
* The listener that we use in order to handle incoming messages.
*/
@SuppressWarnings("unchecked")
private class SmackMessageListener
implements PacketListener
{
/**
* Handles incoming messages and dispatches whatever events are
* necessary.
* @param packet the packet that we need to handle (if it is a message).
*/
public void processPacket(Packet packet)
{
if(!(packet instanceof org.jivesoftware.smack.packet.Message))
return;
org.jivesoftware.smack.packet.Message msg =
(org.jivesoftware.smack.packet.Message)packet;
boolean isForwardedSentMessage = false;
if(msg.getBody() == null)
{
CarbonPacketExtension carbonExt
= (CarbonPacketExtension) msg.getExtension(
CarbonPacketExtension.NAMESPACE);
if(carbonExt == null)
return;
isForwardedSentMessage
= (carbonExt.getElementName()
== CarbonPacketExtension.SENT_ELEMENT_NAME);
List<ForwardedPacketExtension> extensions
= carbonExt.getChildExtensionsOfType(
ForwardedPacketExtension.class);
if(extensions.isEmpty())
return;
// according to xep-0280 all carbons should come from
// our bare jid
if (!msg.getFrom().equals(
StringUtils.parseBareAddress(
jabberProvider.getOurJID())))
{
logger.info("Received a carbon copy with wrong from!");
return;
}
ForwardedPacketExtension forwardedExt = extensions.get(0);
msg = forwardedExt.getMessage();
if(msg == null || msg.getBody() == null)
return;
}
Object multiChatExtension =
msg.getExtension("x", "http://jabber.org/protocol/muc#user");
// its not for us
if(multiChatExtension != null)
return;
String userFullId
= isForwardedSentMessage? msg.getTo() : msg.getFrom();
String userBareID = StringUtils.parseBareAddress(userFullId);
boolean isPrivateMessaging = false;
ChatRoom privateContactRoom = null;
OperationSetMultiUserChatJabberImpl mucOpSet =
(OperationSetMultiUserChatJabberImpl)jabberProvider
.getOperationSet(OperationSetMultiUserChat.class);
if(mucOpSet != null)
privateContactRoom = mucOpSet.getChatRoom(userBareID);
if(privateContactRoom != null)
{
isPrivateMessaging = true;
}
if(logger.isDebugEnabled())
{
if (logger.isDebugEnabled())
logger.debug("Received from "
+ userBareID
+ " the message "
+ msg.toXML());
}
Message newMessage = createMessage(msg.getBody(),
DEFAULT_MIME_TYPE, msg.getPacketID());
//check if the message is available in xhtml
PacketExtension ext = msg.getExtension(
"http://jabber.org/protocol/xhtml-im");
if(ext != null)
{
XHTMLExtension xhtmlExt
= (XHTMLExtension)ext;
//parse all bodies
Iterator<String> bodies = xhtmlExt.getBodies();
StringBuffer messageBuff = new StringBuffer();
while (bodies.hasNext())
{
String body = bodies.next();
messageBuff.append(body);
}
if (messageBuff.length() > 0)
{
// we remove body tags around message cause their
// end body tag is breaking
// the visualization as html in the UI
String receivedMessage =
messageBuff.toString()
// removes body start tag
.replaceAll("\\<[bB][oO][dD][yY].*?>","")
// removes body end tag
.replaceAll("\\</[bB][oO][dD][yY].*?>","");
// for some reason ' is not rendered correctly
// from our ui, lets use its equivalent. Other
// similar chars(< > & ") seem ok.
receivedMessage =
receivedMessage.replaceAll("'", "'");
newMessage = createMessage(receivedMessage,
HTML_MIME_TYPE, msg.getPacketID());
}
}
PacketExtension correctionExtension =
msg.getExtension(MessageCorrectionExtension.NAMESPACE);
String correctedMessageUID = null;
if (correctionExtension != null)
{
correctedMessageUID = ((MessageCorrectionExtension)
correctionExtension).getCorrectedMessageUID();
}
Contact sourceContact
= opSetPersPresence.findContactByID(
(isPrivateMessaging? userFullId : userBareID));
if(msg.getType()
== org.jivesoftware.smack.packet.Message.Type.error)
{
// error which is multichat and we don't know about the contact
// is a muc message error which is missing muc extension
// and is coming from the room, when we try to send message to
// room which was deleted or offline on the server
if(isPrivateMessaging && sourceContact == null)
{
if(privateContactRoom != null)
{
XMPPError error = packet.getError();
int errorResultCode
= ChatRoomMessageDeliveryFailedEvent.UNKNOWN_ERROR;
if(error != null && error.getCode() == 403)
{
errorResultCode
= ChatRoomMessageDeliveryFailedEvent.FORBIDDEN;
}
String errorReason = error.getMessage();
ChatRoomMessageDeliveryFailedEvent evt =
new ChatRoomMessageDeliveryFailedEvent(
privateContactRoom,
null,
errorResultCode,
errorReason,
new Date(),
newMessage);
((ChatRoomJabberImpl)privateContactRoom)
.fireMessageEvent(evt);
}
return;
}
if (logger.isInfoEnabled())
logger.info("Message error received from " + userBareID);
int errorResultCode = MessageDeliveryFailedEvent.UNKNOWN_ERROR;
if (packet.getError() != null)
{
int errorCode = packet.getError().getCode();
if(errorCode == 503)
{
org.jivesoftware.smackx.packet.MessageEvent msgEvent =
(org.jivesoftware.smackx.packet.MessageEvent)
packet.getExtension("x", "jabber:x:event");
if(msgEvent != null && msgEvent.isOffline())
{
errorResultCode =
MessageDeliveryFailedEvent
.OFFLINE_MESSAGES_NOT_SUPPORTED;
}
}
}
if (sourceContact == null)
{
sourceContact = opSetPersPresence.createVolatileContact(
userFullId, isPrivateMessaging);
}
MessageDeliveryFailedEvent ev
= new MessageDeliveryFailedEvent(newMessage,
sourceContact,
correctedMessageUID,
errorResultCode);
// ev = messageDeliveryFailedTransform(ev);
if (ev != null)
fireMessageEvent(ev);
return;
}
putJidForAddress(userFullId, msg.getThread());
// In the second condition we filter all group chat messages,
// because they are managed by the multi user chat operation set.
if(sourceContact == null)
{
if (logger.isDebugEnabled())
logger.debug("received a message from an unknown contact: "
+ userBareID);
//create the volatile contact
sourceContact = opSetPersPresence
.createVolatileContact(
userFullId,
isPrivateMessaging);
}
Date timestamp = new Date();
//Check for XEP-0091 timestamp (deprecated)
PacketExtension delay = msg.getExtension("x", "jabber:x:delay");
if(delay != null && delay instanceof DelayInformation)
{
timestamp = ((DelayInformation)delay).getStamp();
}
//check for XEP-0203 timestamp
delay = msg.getExtension("delay", "urn:xmpp:delay");
if(delay != null && delay instanceof DelayInfo)
{
timestamp = ((DelayInfo)delay).getStamp();
}
ContactResource resource = ((ContactJabberImpl) sourceContact)
.getResourceFromJid(userFullId);
EventObject msgEvt = null;
if(!isForwardedSentMessage)
msgEvt
= new MessageReceivedEvent( newMessage,
sourceContact,
resource,
timestamp,
correctedMessageUID,
isPrivateMessaging,
privateContactRoom);
else
msgEvt = new MessageDeliveredEvent(newMessage, sourceContact, timestamp);
// msgReceivedEvt = messageReceivedTransform(msgReceivedEvt);
if (msgEvt != null)
fireMessageEvent(msgEvt);
}
}
/**
* A filter that prevents this operation set from handling multi user chat
* messages.
*/
private static class GroupMessagePacketFilter implements PacketFilter
{
/**
* Returns <tt>true</tt> if <tt>packet</tt> is a <tt>Message</tt> and
* false otherwise.
*
* @param packet the packet that we need to check.
*
* @return <tt>true</tt> if <tt>packet</tt> is a <tt>Message</tt> and
* false otherwise.
*/
public boolean accept(Packet packet)
{
if(!(packet instanceof org.jivesoftware.smack.packet.Message))
return false;
org.jivesoftware.smack.packet.Message msg
= (org.jivesoftware.smack.packet.Message) packet;
return
!msg.getType().equals(
org.jivesoftware.smack.packet.Message.Type.groupchat);
}
}
/**
* Subscribes this provider as interested in receiving notifications for
* new mail messages from Google mail services such as Gmail or Google Apps.
*/
private void subscribeForGmailNotifications()
{
// first check support for the notification service
String accountIDService = jabberProvider.getAccountID().getService();
boolean notificationsAreSupported
= jabberProvider.isFeatureSupported(
accountIDService,
NewMailNotificationIQ.NAMESPACE);
if (!notificationsAreSupported)
{
if (logger.isDebugEnabled())
logger.debug(accountIDService
+" does not seem to provide a Gmail notification "
+" service so we won't be trying to subscribe for it");
return;
}
if (logger.isDebugEnabled())
logger.debug(accountIDService
+" seems to provide a Gmail notification "
+" service so we will try to subscribe for it");
ProviderManager providerManager = ProviderManager.getInstance();
providerManager.addIQProvider(
MailboxIQ.ELEMENT_NAME,
MailboxIQ.NAMESPACE,
new MailboxIQProvider());
providerManager.addIQProvider(
NewMailNotificationIQ.ELEMENT_NAME,
NewMailNotificationIQ.NAMESPACE,
new NewMailNotificationProvider());
Connection connection = jabberProvider.getConnection();
connection.addPacketListener(
new MailboxIQListener(), new PacketTypeFilter(MailboxIQ.class));
connection.addPacketListener(
new NewMailNotificationListener(),
new PacketTypeFilter(NewMailNotificationIQ.class));
if(opSetPersPresence.getCurrentStatusMessage().equals(
JabberStatusEnum.OFFLINE))
return;
//create a query with -1 values for newer-than-tid and
//newer-than-time attributes
MailboxQueryIQ mailboxQuery = new MailboxQueryIQ();
if (logger.isTraceEnabled())
logger.trace("sending mailNotification for acc: "
+ jabberProvider.getAccountID().getAccountUniqueID());
jabberProvider.getConnection().sendPacket(mailboxQuery);
}
/**
* Creates an html description of the specified mailbox.
*
* @param mailboxIQ the mailboxIQ that we are to describe.
*
* @return an html description of <tt>mailboxIQ</tt>
*/
private String createMailboxDescription(MailboxIQ mailboxIQ)
{
int threadCount = mailboxIQ.getThreadCount();
String resourceHeaderKey = threadCount > 1
? "service.gui.NEW_GMAIL_MANY_HEADER"
: "service.gui.NEW_GMAIL_HEADER";
String resourceFooterKey = threadCount > 1
? "service.gui.NEW_GMAIL_MANY_FOOTER"
: "service.gui.NEW_GMAIL_FOOTER";
// FIXME Escape HTML!
String newMailHeader = JabberActivator.getResources().getI18NString(
resourceHeaderKey,
new String[]
{
jabberProvider.getAccountID()
.getService(), //{0} - service name
mailboxIQ.getUrl(), //{1} - inbox URI
Integer.toString( threadCount )//{2} - thread count
});
StringBuilder message = new StringBuilder(newMailHeader);
//we now start an html table for the threads.
message.append("<table width=100% cellpadding=2 cellspacing=0 ");
message.append("border=0 bgcolor=#e8eef7>");
Iterator<MailThreadInfo> threads = mailboxIQ.threads();
String maxThreadsStr = (String)JabberActivator.getConfigurationService()
.getProperty(PNAME_MAX_GMAIL_THREADS_PER_NOTIFICATION);
int maxThreads = 5;
try
{
if(maxThreadsStr != null)
maxThreads = Integer.parseInt(maxThreadsStr);
}
catch (NumberFormatException e)
{
if (logger.isDebugEnabled())
logger.debug("Failed to parse max threads count: "+maxThreads
+". Going for default.");
}
//print a maximum of MAX_THREADS
for (int i = 0; i < maxThreads && threads.hasNext(); i++)
{
message.append(threads.next().createHtmlDescription());
}
message.append("</table><br/>");
if(threadCount > maxThreads)
{
String messageFooter = JabberActivator.getResources().getI18NString(
resourceFooterKey,
new String[]
{
mailboxIQ.getUrl(), //{0} - inbox URI
Integer.toString(
threadCount - maxThreads )//{1} - thread count
});
message.append(messageFooter);
}
return message.toString();
}
public String getRecentJIDForAddress(String address)
{
return recentJIDForAddress.get(address);
}
/**
* Receives incoming MailNotification Packets
*/
private class MailboxIQListener
implements PacketListener
{
/**
* Handles incoming <tt>MailboxIQ</tt> packets.
*
* @param packet the IQ that we need to handle in case it is a
* <tt>MailboxIQ</tt>.
*/
public void processPacket(Packet packet)
{
if(packet != null && !(packet instanceof MailboxIQ))
return;
MailboxIQ mailboxIQ = (MailboxIQ) packet;
if(mailboxIQ.getTotalMatched() < 1)
return;
//Get a reference to a dummy volatile contact
Contact sourceContact = opSetPersPresence
.findContactByID(jabberProvider.getAccountID().getService());
if(sourceContact == null)
sourceContact = opSetPersPresence.createVolatileContact(
jabberProvider.getAccountID().getService());
lastReceivedMailboxResultTime = mailboxIQ.getResultTime();
String newMail = createMailboxDescription(mailboxIQ);
Message newMailMessage = new MessageJabberImpl(
newMail, HTML_MIME_TYPE, DEFAULT_MIME_ENCODING, null);
MessageReceivedEvent msgReceivedEvt = new MessageReceivedEvent(
newMailMessage, sourceContact, new Date(),
MessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED);
fireMessageEvent(msgReceivedEvt);
}
}
/**
* Receives incoming NewMailNotification Packets.
*/
private class NewMailNotificationListener
implements PacketListener
{
/**
* Handles incoming <tt>NewMailNotificationIQ</tt> packets.
*
* @param packet the IQ that we need to handle in case it is a
* <tt>NewMailNotificationIQ</tt>.
*/
public void processPacket(Packet packet)
{
if(packet != null && !(packet instanceof NewMailNotificationIQ))
return;
//check whether we are still enabled.
boolean enableGmailNotifications
= jabberProvider
.getAccountID()
.getAccountPropertyBoolean(
"GMAIL_NOTIFICATIONS_ENABLED",
false);
if (!enableGmailNotifications)
return;
if(opSetPersPresence.getCurrentStatusMessage()
.equals(JabberStatusEnum.OFFLINE))
return;
MailboxQueryIQ mailboxQueryIQ = new MailboxQueryIQ();
if(lastReceivedMailboxResultTime != -1)
mailboxQueryIQ.setNewerThanTime(
lastReceivedMailboxResultTime);
if (logger.isTraceEnabled())
logger.trace(
"send mailNotification for acc: "
+ jabberProvider.getAccountID().getAccountUniqueID());
jabberProvider.getConnection().sendPacket(mailboxQueryIQ);
}
}
/**
* Returns the inactivity timeout in milliseconds.
*
* @return The inactivity timeout in milliseconds. Or -1 if undefined
*/
public long getInactivityTimeout()
{
return JID_INACTIVITY_TIMEOUT;
}
/**
* Adds additional filters for incoming messages. To be able to skip some
* messages.
* @param filter to add
*/
public void addMessageFilters(PacketFilter filter)
{
this.packetFilters.add(filter);
}
/**
* Returns the next unique thread id. Each thread id made up of a short
* alphanumeric prefix along with a unique numeric value.
*
* @return the next thread id.
*/
public static synchronized String nextThreadID() {
return prefix + Long.toString(id++);
}
}