title: PyAPNs sending push notification to more than one device token not working
body: <p>I am using PyAPNs for sending iOS Push Notification. I also merged the fixes for following known issues</p>
  
  <p><a href="https://github.com/djacobs/PyAPNs/issues/13" rel="noreferrer">https://github.com/djacobs/PyAPNs/issues/13</a></p>
  
  <p>Now, the code is working fine If I send notification to an individual device. But I have a list for device tokens and I have to send notification to all of them one by one. For this purpose I am simple looping over the single notification call like this:</p>
  
  <pre><code>def send_notifications(self, tokens, payload):
      for token in tokens:
          try :
              logging.info("Sending Notification to Token: %s" % (token))
              self.send_notification(token, payload)                
          except Exception, e:
              self._disconnect()
              logging.info("Exception: %s" % (str(e)))
              logging.info("Token: %s" % (token))
  </code></pre>
  
  <p>But the problem is that above code is not working. The device token which was working fine for individual push, is not working using above code. For example  device token 45183e79de216ea05e3d6e83083476ebeb64caf733188bb77b0b1d268526c815 is working fine individually but failing in case of bulk send. For reference I am putting the apns file and partial server logs:</p>
  
  <p>apns.py</p>
  
  <pre><code># PyAPNs was developed by Simon Whitaker &lt;simon@goosoftware.co.uk&gt;
  # Source available at https://github.com/simonwhitaker/PyAPNs
  #
  # PyAPNs is distributed under the terms of the MIT license.
  #
  # Copyright (c) 2011 Goo Software Ltd
  #
  # Permission is hereby granted, free of charge, to any person obtaining a copy of
  # this software and associated documentation files (the "Software"), to deal in
  # the Software without restriction, including without limitation the rights to
  # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  # of the Software, and to permit persons to whom the Software is furnished to do
  # so, subject to the following conditions:
  #
  # The above copyright notice and this permission notice shall be included in all
  # copies or substantial portions of the Software.
  #
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  # SOFTWARE.
  
  from binascii import a2b_hex, b2a_hex
  from datetime import datetime, timedelta
  from time import mktime
  from socket import socket, AF_INET, SOCK_STREAM, timeout
  from struct import pack, unpack
  
  import select
  
  try:
      from ssl import wrap_socket
      from ssl import SSLError, SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE
  except ImportError:
      from socket import ssl as wrap_socket
  
  try:
      import json
  except ImportError:
      import simplejson as json
  
  from apnserrors import *
  
  import logging
  import StringIO
  
  MAX_PAYLOAD_LENGTH = 256
  TIMEOUT = 60
  ERROR_RESPONSE_LENGTH = 6
  
  class APNs(object):
      """A class representing an Apple Push Notification service connection"""
  
      def __init__(self, use_sandbox=False, cert_file=None, key_file=None, enhanced=True):
          """
          Set use_sandbox to True to use the sandbox (test) APNs servers.
          Default is False.
          """
          super(APNs, self).__init__()
          self.use_sandbox = use_sandbox
          self.cert_file = cert_file
          self.key_file = key_file
          self.enhanced = enhanced
          self._feedback_connection = None
          self._gateway_connection = None
  
      @staticmethod
      def unpacked_uchar_big_endian(byte):
          """
          Returns an unsigned char from a packed big-endian (network) byte
          """
          return unpack('&gt;B', byte)[0]
  
      @staticmethod
      def packed_ushort_big_endian(num):
          """
          Returns an unsigned short in packed big-endian (network) form
          """
          return pack('&gt;H', num)
  
      @staticmethod
      def unpacked_ushort_big_endian(bytes):
          """
          Returns an unsigned short from a packed big-endian (network) byte
          array
          """
          return unpack('&gt;H', bytes)[0]
  
      @staticmethod
      def packed_uint_big_endian(num):
          """
          Returns an unsigned int in packed big-endian (network) form
          """
          return pack('&gt;I', num)
  
      @staticmethod
      def unpacked_uint_big_endian(bytes):
          """
          Returns an unsigned int from a packed big-endian (network) byte array
          """
          return unpack('&gt;I', bytes)[0]
  
      @property
      def feedback_server(self):
          if not self._feedback_connection:
              self._feedback_connection = FeedbackConnection(
                  use_sandbox = self.use_sandbox,
                  cert_file = self.cert_file,
                  key_file = self.key_file
              )
          return self._feedback_connection
  
      @property
      def gateway_server(self):
          if not self._gateway_connection:
              self._gateway_connection = GatewayConnection(
                  use_sandbox = self.use_sandbox,
                  cert_file = self.cert_file,
                  key_file = self.key_file,
                  enhanced = self.enhanced
              )
          return self._gateway_connection
  
  
  class APNsConnection(object):
      """
      A generic connection class for communicating with the APNs
      """
      def __init__(self, cert_file=None, key_file=None, enhanced=True):
          super(APNsConnection, self).__init__()
          self.cert_file = cert_file
          self.key_file = key_file
          self.enhanced = enhanced
          self._socket = None
          self._ssl = None
  
      def __del__(self):
          self._disconnect();
  
      def _connect(self):
          # Establish an SSL connection
          self._socket = socket(AF_INET, SOCK_STREAM)
          self._socket.connect((self.server, self.port))
  
          if self.enhanced:
              self._ssl = wrap_socket(self._socket, StringIO.StringIO(self.key_file), StringIO.StringIO(self.cert_file),
                                      do_handshake_on_connect=False)
              self._ssl.setblocking(0)
              while True:
                  try:
                      self._ssl.do_handshake()
                      break
                  except SSLError, err:
                      if SSL_ERROR_WANT_READ == err.args[0]:
                          select.select([self._ssl], [], [])
                      elif SSL_ERROR_WANT_WRITE == err.args[0]:
                          select.select([], [self._ssl], [])
                      else:
                          raise
          else:
              self._ssl = wrap_socket(self._socket, StringIO.StringIO(self.key_file), StringIO.StringIO(self.cert_file))
  
      def _disconnect(self):
          if self._socket:
              self._socket.close()
              self._ssl = None
  
      def _connection(self):
          if not self._ssl:
              self._connect()
          return self._ssl
  
      def read(self, n=None):
          return self._connection().recv(n)
  
      def recvall(self, n):
          data = ""
          while True:
              more = self._connection().recv(n - len(data))
              data += more
              if len(data) &gt;= n:
                  break
              rlist, _, _ = select.select([self._connection()], [], [], TIMEOUT)
              if not rlist:
                  raise timeout
  
          return data
  
      def write(self, string):
          if self.enhanced: # nonblocking socket
              rlist, _, _ = select.select([self._connection()], [], [], 0)
  
              if rlist: # there's error response from APNs
                  buff = self.recvall(ERROR_RESPONSE_LENGTH)
                  if len(buff) != ERROR_RESPONSE_LENGTH:
                      return None
  
                  command = APNs.unpacked_uchar_big_endian(buff[0])
  
                  if 8 != command:
                      self._disconnect()
                      raise UnknownError(0)
  
                  status = APNs.unpacked_uchar_big_endian(buff[1])
                  identifier = APNs.unpacked_uint_big_endian(buff[2:6])
  
                  self._disconnect()
  
                  raise { 1: ProcessingError,
                          2: MissingDeviceTokenError,
                          3: MissingTopicError,
                          4: MissingPayloadError,
                          5: InvalidTokenSizeError,
                          6: InvalidTopicSizeError,
                          7: InvalidPayloadSizeError,
                          8: InvalidTokenError }.get(status, UnknownError)(identifier)
  
              _, wlist, _ = select.select([], [self._connection()], [], TIMEOUT)
              if wlist:
                  return self._connection().sendall(string)
              else:
                  self._disconnect()
                  raise timeout
  
          else: # not-enhanced format using blocking socket
              return self._connection().sendall(string)
  
  class PayloadAlert(object):
      def __init__(self, body, action_loc_key=None, loc_key=None,
                   loc_args=None, launch_image=None):
          super(PayloadAlert, self).__init__()
          self.body = body
          self.action_loc_key = action_loc_key
          self.loc_key = loc_key
          self.loc_args = loc_args
          self.launch_image = launch_image
  
      def dict(self):
          d = { 'body': self.body }
          if self.action_loc_key:
              d['action-loc-key'] = self.action_loc_key
          if self.loc_key:
              d['loc-key'] = self.loc_key
          if self.loc_args:
              d['loc-args'] = self.loc_args
          if self.launch_image:
              d['launch-image'] = self.launch_image
          return d
  
  class Payload(object):
      """A class representing an APNs message payload"""
      def __init__(self, alert=None, badge=None, sound=None, custom={}):
          super(Payload, self).__init__()
          self.alert = alert
          self.badge = badge
          self.sound = sound
          self.custom = custom
          self._check_size()
  
      def dict(self):
          """Returns the payload as a regular Python dictionary"""
          d = {}
          if self.alert:
              # Alert can be either a string or a PayloadAlert
              # object
              if isinstance(self.alert, PayloadAlert):
                  d['alert'] = self.alert.dict()
              else:
                  d['alert'] = self.alert
          if self.sound:
              d['sound'] = self.sound
          if self.badge is not None:
              d['badge'] = int(self.badge)
  
          d = { 'aps': d }
          d.update(self.custom)
          return d
  
      def json(self):
          return json.dumps(self.dict(), separators=(',',':'), ensure_ascii=False).encode('utf-8')
  
      def _check_size(self):
          if len(self.json()) &gt; MAX_PAYLOAD_LENGTH:
              raise PayloadTooLargeError()
  
      def __repr__(self):
          attrs = ("alert", "badge", "sound", "custom")
          args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs])
          return "%s(%s)" % (self.__class__.__name__, args)
  
  
  class FeedbackConnection(APNsConnection):
      """
      A class representing a connection to the APNs Feedback server
      """
      def __init__(self, use_sandbox=False, **kwargs):
          super(FeedbackConnection, self).__init__(**kwargs)
          self.server = (
              'feedback.push.apple.com',
              'feedback.sandbox.push.apple.com')[use_sandbox]
          self.port = 2196
  
      def _chunks(self):
          BUF_SIZE = 4096
          while 1:
              data = self.read(BUF_SIZE)
              yield data
              if not data:
                  break
  
      def items(self):
          """
          A generator that yields (token_hex, fail_time) pairs retrieved from
          the APNs feedback server
          """
          buff = ''
          for chunk in self._chunks():
              buff += chunk
  
              # Quit if there's no more data to read
              if not buff:
                  break
  
              # Sanity check: after a socket read we should always have at least
              # 6 bytes in the buffer
              if len(buff) &lt; 6:
                  break
  
              while len(buff) &gt; 6:
                  token_length = APNs.unpacked_ushort_big_endian(buff[4:6])
                  bytes_to_read = 6 + token_length
                  if len(buff) &gt;= bytes_to_read:
                      fail_time_unix = APNs.unpacked_uint_big_endian(buff[0:4])
                      fail_time = datetime.utcfromtimestamp(fail_time_unix)
                      token = b2a_hex(buff[6:bytes_to_read])
  
                      yield (token, fail_time)
  
                      # Remove data for current token from buffer
                      buff = buff[bytes_to_read:]
                  else:
                      # break out of inner while loop - i.e. go and fetch
                      # some more data and append to buffer
                      break
  
  class GatewayConnection(APNsConnection):
      """
      A class that represents a connection to the APNs gateway server
      """
      def __init__(self, use_sandbox=False, **kwargs):
          super(GatewayConnection, self).__init__(**kwargs)
          self.server = (
              'gateway.push.apple.com',
              'gateway.sandbox.push.apple.com')[use_sandbox]
          self.port = 2195
  
      def _get_notification(self, token_hex, payload):
          """
          Takes a token as a hex string and a payload as a Python dict and sends
          the notification
          """
          token_bin = a2b_hex(token_hex)
          token_length_bin = APNs.packed_ushort_big_endian(len(token_bin))
          payload_json = payload.json()
          payload_length_bin = APNs.packed_ushort_big_endian(len(payload_json))
  
          notification = ('\0' + token_length_bin + token_bin
                          + payload_length_bin + payload_json)
  
          return notification
  
      def _get_enhanced_notification(self, token_hex, payload, identifier, expiry):
          """
          Takes a token as a hex string and a payload as a Python dict and sends
          the notification in the enhanced format
          """
          token_bin = a2b_hex(token_hex)
          token_length_bin = APNs.packed_ushort_big_endian(len(token_bin))
          payload_json = payload.json()
          payload_length_bin = APNs.packed_ushort_big_endian(len(payload_json))
          identifier_bin = APNs.packed_uint_big_endian(identifier)
          expiry_bin = APNs.packed_uint_big_endian(int(mktime(expiry.timetuple())))
  
          notification = ('\1' + identifier_bin + expiry_bin + token_length_bin + token_bin
                          + payload_length_bin + payload_json)
  
          return notification
  
      def send_notification(self, token_hex, payload, identifier=None, expiry=None):
          if self.enhanced:
              if not expiry: # by default, undelivered notification expires after 30 seconds
                  expiry = datetime.utcnow() + timedelta(30)
              if not identifier:
                  identifier = 0
  
              logging.info("self.write(self._get_enhanced_notification())")    
              self.write(self._get_enhanced_notification(token_hex, payload, identifier,
                                                         expiry))
          else:
              logging.info("self.write(self._get_notification(token_hex, payload))")
              self.write(self._get_notification(token_hex, payload))
  
      def send_notifications(self, tokens, payload):
          for token in tokens:
              try :
                  logging.info("Sending Notification to Token: %s" % (token))
                  self.send_notification(token, payload)                
              except Exception, e:
                  self._disconnect()
                  logging.info("Exception: %s" % (str(e)))
                  logging.info("Token: %s" % (token))
  </code></pre>
  
  <p>Server Logs:</p>
  
  <pre><code>    Sending Notification to Token: 99f65209a76ed41ce50c73198d72048f94085dd2a2dde0245110dccccda86fd0
      I 2014-05-20 05:18:24.029
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:24.437
      Sending Notification to Token: 2230c2421e3b83cd6b16a69c6ba528230b11d29183b0bfb73b159816237b17ce
      I 2014-05-20 05:18:24.437
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:24.442
  .
  .
  .
      Sending Notification to Token: 6bd80feb5158a8f92537955c93acd1661242c007dcffebe55e77bb38cafef0ba
      I 2014-05-20 05:18:24.986
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:24.991
      Sending Notification to Token: 2230c2421e3b83cd6b16a69c6ba528230b11d29183b0bfb73b159816237b17ce
      I 2014-05-20 05:18:24.991
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:24.996
      Sending Notification to Token: 6bd80feb5158a8f92537955c93acd1661242c007dcffebe55e77bb38cafef0ba
      I 2014-05-20 05:18:24.996
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:25.004
      Sending Notification to Token: 1bacfcb6b80868493b236ec6131bed11918c935752734701b89b060045e6b006
      I 2014-05-20 05:18:25.004
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:25.021
      Sending Notification to Token: 35bd8dda849e30a85b12b2a0e274b9507db7c7f365aa5a27f3fbda316052246e
      I 2014-05-20 05:18:25.021
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:25.054
      Sending Notification to Token: 6bd80feb5158a8f92537955c93acd1661242c007dcffebe55e77bb38cafef0ba
      I 2014-05-20 05:18:25.054
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:25.059
      Sending Notification to Token: 6bd80feb5158a8f92537955c93acd1661242c007dcffebe55e77bb38cafef0ba
      I 2014-05-20 05:18:25.059
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:25.064
      Sending Notification to Token: 1bacfcb6b80868493b236ec6131bed11918c935752734701b89b060045e6b006
      I 2014-05-20 05:18:25.064
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:25.068
      Sending Notification to Token: 6bd80feb5158a8f92537955c93acd1661242c007dcffebe55e77bb38cafef0ba
      I 2014-05-20 05:18:25.069
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:25.073
      Sending Notification to Token: d25a34a1fd031abf3fbfb5916af415206048fb6343586b91b96d0506eb28cb54
      I 2014-05-20 05:18:25.073
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:25.078
  
  .
  .
  .
      Sending Notification to Token: 45183e79de216ea05e3d6e83083476ebeb64caf733188bb77b0b1d268526c815
      I 2014-05-20 05:18:30.145
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:30.152
      Sending Notification to Token: b57b2d96a4b4db552137bcea4fd58f3ce53393fbe7c828b617306df2922dbfd3
      I 2014-05-20 05:18:30.152
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:30.159
      Sending Notification to Token: 82acbf3dc5da893d2f4d551df10c129c8c192efe335cc608d291dc922e947615
      I 2014-05-20 05:18:30.159
      self.write(self._get_enhanced_notification())
      I 2014-05-20 05:18:30.166
  
      feedback token_hex: 0cf58d47f435f170473b63e1852b637c11935b6e38d41321fe98911eaf898301
      I 2014-05-20 05:18:31.754
      feedback token_hex: 0d344046d62f808c30bc5670cbb7dc478cca0a9798830d22f8f6ed27c76923c6
      I 2014-05-20 05:18:31.754
      feedback token_hex: 2230c2421e3b83cd6b16a69c6ba528230b11d29183b0bfb73b159816237b17ce
      I 2014-05-20 05:18:31.754
      feedback token_hex: 349c54d18bb1ee014dc84f7b7b60c4a2eef1b9d3cf51c12daab93261d5e09e7c
      I 2014-05-20 05:18:31.754
      feedback token_hex: 3980924c6cd4e752f2a02b8d28f7ce11d7a3eba5f41628166733cda4e621bfcf
      I 2014-05-20 05:18:31.755
      feedback token_hex: 6bd80feb5158a8f92537955c93acd1661242c007dcffebe55e77bb38cafef0ba
      I 2014-05-20 05:18:31.755
      feedback token_hex: b96e27adab644f0a18e8f4dfe19786aab82b69e1ef46c580b887e6779964c55f
      I 2014-05-20 05:18:31.755
      feedback token_hex: e5ee1848342d2e4789cfa07baae3ac754785d78ccb50dc5b5f10044053843115
      I 2014-05-20 05:18:31.755
      feedback token_hex: f339e53e44efa03996dffc24b5c9419609018fd8dd5d1953230a4bd8c5cabc78
      I 2014-05-20 05:18:31.760
      feedback fail_count: 9
  </code></pre>