001/**
002 *
003 * Copyright 2003-2006 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smackx.address;
019
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.List;
023
024import org.jivesoftware.smack.SmackException.FeatureNotSupportedException;
025import org.jivesoftware.smack.SmackException.NoResponseException;
026import org.jivesoftware.smack.SmackException.NotConnectedException;
027import org.jivesoftware.smack.XMPPConnection;
028import org.jivesoftware.smack.XMPPException.XMPPErrorException;
029import org.jivesoftware.smack.packet.IQ;
030import org.jivesoftware.smack.packet.Message;
031import org.jivesoftware.smack.packet.Presence;
032import org.jivesoftware.smack.packet.Stanza;
033import org.jivesoftware.smack.packet.StanzaBuilder;
034import org.jivesoftware.smack.packet.StanzaFactory;
035import org.jivesoftware.smack.packet.StanzaView;
036import org.jivesoftware.smack.util.StringUtils;
037
038import org.jivesoftware.smackx.address.packet.MultipleAddresses;
039import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
040
041import org.jxmpp.jid.DomainBareJid;
042import org.jxmpp.jid.EntityBareJid;
043import org.jxmpp.jid.EntityFullJid;
044import org.jxmpp.jid.Jid;
045
046/**
047 * A MultipleRecipientManager allows to send packets to multiple recipients by making use of
048 * <a href="http://www.xmpp.org/extensions/jep-0033.html">XEP-33: Extended Stanza Addressing</a>.
049 * It also allows to send replies to packets that were sent to multiple recipients.
050 *
051 * @author Gaston Dombiak
052 */
053public class MultipleRecipientManager {
054
055    /**
056     * Sends the specified stanza to the collection of specified recipients using the
057     * specified connection. If the server has support for XEP-33 then only one
058     * stanza is going to be sent to the server with the multiple recipient instructions.
059     * However, if XEP-33 is not supported by the server then the client is going to send
060     * the stanza to each recipient.
061     *
062     * @param connection the connection to use to send the packet.
063     * @param packet     the stanza to send to the list of recipients.
064     * @param to         the collection of JIDs to include in the TO list or <code>null</code> if no TO
065     *                   list exists.
066     * @param cc         the collection of JIDs to include in the CC list or <code>null</code> if no CC
067     *                   list exists.
068     * @param bcc        the collection of JIDs to include in the BCC list or <code>null</code> if no BCC
069     *                   list exists.
070     * @throws FeatureNotSupportedException if special XEP-33 features where requested, but the
071     *         server does not support them.
072     * @throws XMPPErrorException if server does not support XEP-33: Extended Stanza Addressing and
073     *                       some XEP-33 specific features were requested.
074     * @throws NoResponseException if there was no response from the server.
075     * @throws NotConnectedException if the XMPP connection is not connected.
076     * @throws InterruptedException if the calling thread was interrupted.
077     */
078    public static void send(XMPPConnection connection, Stanza packet, Collection<? extends Jid> to,
079            Collection<? extends Jid> cc, Collection<? extends Jid> bcc) throws NoResponseException, XMPPErrorException,
080            FeatureNotSupportedException, NotConnectedException, InterruptedException {
081        send(connection, packet, to, cc, bcc, null, null, false);
082    }
083
084    /**
085     * Sends the specified stanza to the collection of specified recipients using the specified
086     * connection. If the server has support for XEP-33 then only one stanza is going to be sent to
087     * the server with the multiple recipient instructions. However, if XEP-33 is not supported by
088     * the server then the client is going to send the stanza to each recipient.
089     *
090     * @param connection the connection to use to send the packet.
091     * @param packet the stanza to send to the list of recipients.
092     * @param to the collection of JIDs to include in the TO list or <code>null</code> if no TO list exists.
093     * @param cc the collection of JIDs to include in the CC list or <code>null</code> if no CC list exists.
094     * @param bcc the collection of JIDs to include in the BCC list or <code>null</code> if no BCC list
095     *        exists.
096     * @param replyTo address to which all replies are requested to be sent or <code>null</code>
097     *        indicating that they can reply to any address.
098     * @param replyRoom JID of a MUC room to which responses should be sent or <code>null</code>
099     *        indicating that they can reply to any address.
100     * @param noReply true means that receivers should not reply to the message.
101     * @throws XMPPErrorException if server does not support XEP-33: Extended Stanza Addressing and
102     *         some XEP-33 specific features were requested.
103     * @throws NoResponseException if there was no response from the server.
104     * @throws FeatureNotSupportedException if special XEP-33 features where requested, but the
105     *         server does not support them.
106     * @throws NotConnectedException if the XMPP connection is not connected.
107     * @throws InterruptedException if the calling thread was interrupted.
108     */
109    public static void send(XMPPConnection connection, Stanza packet, Collection<? extends Jid> to, Collection<? extends Jid> cc, Collection<? extends Jid> bcc,
110            Jid replyTo, Jid replyRoom, boolean noReply) throws NoResponseException, XMPPErrorException, FeatureNotSupportedException, NotConnectedException, InterruptedException {
111        // Check if *only* 'to' is set and contains just *one* entry, in this case extended stanzas addressing is not
112        // required at all and we can send it just as normal stanza without needing to add the extension element
113        if (to != null && to.size() == 1 && (cc == null || cc.isEmpty()) && (bcc == null || bcc.isEmpty()) && !noReply
114                        && StringUtils.isNullOrEmpty(replyTo) && StringUtils.isNullOrEmpty(replyRoom)) {
115            Jid toJid = to.iterator().next();
116            packet.setTo(toJid);
117            connection.sendStanza(packet);
118            return;
119        }
120        DomainBareJid serviceAddress = getMultipleRecipientServiceAddress(connection);
121        if (serviceAddress != null) {
122            // Send packet to target users using multiple recipient service provided by the server
123            sendThroughService(connection, packet, to, cc, bcc, replyTo, replyRoom, noReply,
124                    serviceAddress);
125        }
126        else {
127            // Server does not support XEP-33 so try to send the packet to each recipient
128            if (noReply || replyTo != null ||
129                    replyRoom != null) {
130                // Some specified XEP-33 features were requested so throw an exception alerting
131                // the user that this features are not available
132                throw new FeatureNotSupportedException("Extended Stanza Addressing");
133            }
134            // Send the packet to each individual recipient
135            sendToIndividualRecipients(connection, packet, to, cc, bcc);
136        }
137    }
138
139    /**
140     * Sends a reply to a previously received stanza that was sent to multiple recipients. Before
141     * attempting to send the reply message some checks are performed. If any of those checks
142     * fails, then an XMPPException is going to be thrown with the specific error detail.
143     *
144     * @param connection the connection to use to send the reply.
145     * @param original   the previously received stanza that was sent to multiple recipients.
146     * @param reply      the new message to send as a reply.
147     * @throws XMPPErrorException if there was an XMPP error returned.
148     * @throws InterruptedException if the calling thread was interrupted.
149     * @throws NotConnectedException if the XMPP connection is not connected.
150     * @throws FeatureNotSupportedException if a requested feature is not supported by the remote entity.
151     * @throws NoResponseException if there was no response from the remote entity.
152     */
153    public static void reply(XMPPConnection connection, Message original, Message reply)
154            throws XMPPErrorException, InterruptedException, NotConnectedException, NoResponseException, FeatureNotSupportedException {
155        MultipleRecipientInfo info = getMultipleRecipientInfo(original);
156        if (info == null) {
157            throw new IllegalArgumentException("Original message does not contain multiple recipient info");
158        }
159        if (info.shouldNotReply()) {
160            throw new IllegalArgumentException("Original message should not be replied");
161        }
162        if (info.getReplyRoom() != null) {
163            throw new IllegalArgumentException("Reply should be sent through a room");
164        }
165        // Any <thread/> element from the initial message MUST be copied into the reply.
166        if (original.getThread() != null) {
167            reply.asBuilder().setThread(original.getThread()).build();
168        }
169        MultipleAddresses.Address replyAddress = info.getReplyAddress();
170        if (replyAddress != null && replyAddress.getJid() != null) {
171            // Send reply to the reply_to address
172            reply.setTo(replyAddress.getJid());
173            connection.sendStanza(reply);
174        }
175        else {
176            // Send reply to multiple recipients
177            List<Jid> to = new ArrayList<>(info.getTOAddresses().size());
178            List<Jid> cc = new ArrayList<>(info.getCCAddresses().size());
179            for (MultipleAddresses.Address jid : info.getTOAddresses()) {
180                to.add(jid.getJid());
181            }
182            for (MultipleAddresses.Address jid : info.getCCAddresses()) {
183                cc.add(jid.getJid());
184            }
185            // Add original sender as a 'to' address (if not already present)
186            if (!to.contains(original.getFrom()) && !cc.contains(original.getFrom())) {
187                to.add(original.getFrom());
188            }
189            // Remove the sender from the TO/CC list (try with bare JID too)
190            EntityFullJid from = connection.getUser();
191            if (!to.remove(from) && !cc.remove(from)) {
192                EntityBareJid bareJID = from.asEntityBareJid();
193                to.remove(bareJID);
194                cc.remove(bareJID);
195            }
196
197            send(connection, reply, to, cc, null, null, null, false);
198        }
199    }
200
201    /**
202     * Returns the {@link MultipleRecipientInfo} contained in the specified stanza or
203     * <code>null</code> if none was found. Only packets sent to multiple recipients will
204     * contain such information.
205     *
206     * @param packet the stanza to check.
207     * @return the MultipleRecipientInfo contained in the specified stanza or <code>null</code>
208     *         if none was found.
209     */
210    public static MultipleRecipientInfo getMultipleRecipientInfo(Stanza packet) {
211        MultipleAddresses extension = packet.getExtension(MultipleAddresses.class);
212        return extension == null ? null : new MultipleRecipientInfo(extension);
213    }
214
215    private static void sendToIndividualRecipients(XMPPConnection connection, StanzaView stanza,
216            Collection<? extends Jid> to, Collection<? extends Jid> cc, Collection<? extends Jid> bcc) throws NotConnectedException, InterruptedException {
217        final StanzaFactory stanzaFactory = connection.getStanzaFactory();
218        final StanzaBuilder<?> stanzaBuilder;
219        if (stanza instanceof Message) {
220            Message message = (Message) stanza;
221            stanzaBuilder = stanzaFactory.buildMessageStanzaFrom(message);
222        } else if (stanza instanceof Presence) {
223            Presence presence = (Presence) stanza;
224            stanzaBuilder = stanzaFactory.buildPresenceStanzaFrom(presence);
225        } else if (stanza instanceof IQ) {
226            throw new IllegalArgumentException("IQ stanzas have no supported fallback in case no XEP-0033 service is available");
227        } else {
228            throw new AssertionError();
229        }
230
231        final int numRecipients = to.size() + cc.size() + bcc.size();
232        final List<Jid> recipients = new ArrayList<>(numRecipients);
233
234        if (to != null) {
235            recipients.addAll(to);
236        }
237        if (cc != null) {
238            recipients.addAll(cc);
239        }
240        if (bcc != null) {
241            recipients.addAll(bcc);
242        }
243
244        final List<Stanza> stanzasToSend = new ArrayList<>(numRecipients);
245        for (Jid recipient : recipients) {
246            Stanza stanzaToSend = stanzaBuilder.to(recipient).build();
247            stanzasToSend.add(stanzaToSend);
248        }
249
250        // TODO: Use XMPPConnection.sendStanzas(Collection<? extends Stanza>) once this method exists.
251        for (Stanza stanzaToSend : stanzasToSend) {
252            connection.sendStanza(stanzaToSend);
253        }
254    }
255
256    private static void sendThroughService(XMPPConnection connection, Stanza packet, Collection<? extends Jid> to,
257            Collection<? extends Jid> cc, Collection<? extends Jid> bcc, Jid replyTo, Jid replyRoom, boolean noReply,
258            DomainBareJid serviceAddress) throws NotConnectedException, InterruptedException {
259        // Create multiple recipient extension
260        MultipleAddresses multipleAddresses = new MultipleAddresses();
261        if (to != null) {
262            for (Jid jid : to) {
263                multipleAddresses.addAddress(MultipleAddresses.Type.to, jid, null, null, false, null);
264            }
265        }
266        if (cc != null) {
267            for (Jid jid : cc) {
268                multipleAddresses.addAddress(MultipleAddresses.Type.to, jid, null, null, false, null);
269            }
270        }
271        if (bcc != null) {
272            for (Jid jid : bcc) {
273                multipleAddresses.addAddress(MultipleAddresses.Type.bcc, jid, null, null, false, null);
274            }
275        }
276        if (noReply) {
277            multipleAddresses.setNoReply();
278        }
279        else {
280            if (replyTo != null) {
281                multipleAddresses
282                        .addAddress(MultipleAddresses.Type.replyto, replyTo, null, null, false, null);
283            }
284            if (replyRoom != null) {
285                multipleAddresses.addAddress(MultipleAddresses.Type.replyroom, replyRoom, null, null,
286                        false, null);
287            }
288        }
289        // Set the multiple recipient service address as the target address
290        packet.setTo(serviceAddress);
291        // Add extension to packet
292        packet.addExtension(multipleAddresses);
293        // Send the packet
294        connection.sendStanza(packet);
295    }
296
297    /**
298     * Returns the address of the multiple recipients service. To obtain such address service
299     * discovery is going to be used on the connected server and if none was found then another
300     * attempt will be tried on the server items. The discovered information is going to be
301     * cached for 24 hours.
302     *
303     * @param connection the connection to use for disco. The connected server is going to be
304     *                   queried.
305     * @return the address of the multiple recipients service or <code>null</code> if none was found.
306     * @throws NoResponseException if there was no response from the server.
307     * @throws XMPPErrorException if there was an XMPP error returned.
308     * @throws NotConnectedException if the XMPP connection is not connected.
309     * @throws InterruptedException if the calling thread was interrupted.
310     */
311    private static DomainBareJid getMultipleRecipientServiceAddress(XMPPConnection connection) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
312        ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
313        return sdm.findService(MultipleAddresses.NAMESPACE, true);
314    }
315
316}