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