Full Code of timehop/apns for AI

master c2b19f701c60 cached
21 files
69.5 KB
24.0k tokens
100 symbols
1 requests
Download .txt
Repository: timehop/apns
Branch: master
Commit: c2b19f701c60
Files: 21
Total size: 69.5 KB

Directory structure:
gitextract_u9iwx99q/

├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── apns_suite_test.go
├── badge_number.go
├── badge_number_test.go
├── client.go
├── client_test.go
├── conn.go
├── conn_test.go
├── doc.go
├── error.go
├── error_test.go
├── example/
│   └── example.go
├── feedback.go
├── feedback_test.go
├── go.mod
├── go.sum
├── notification.go
└── notification_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe
*.test

# Example keys
example/*.key
example/*.crt

# Ginkgo files
*.coverprofile
coverage.html


================================================
FILE: .travis.yml
================================================
language: go
go:
  - 1.3
services:
  - redis-server
before_script:
  - go get github.com/onsi/ginkgo
  - go get github.com/onsi/gomega
  - go get code.google.com/p/go.tools/cmd/cover
  - go install github.com/onsi/ginkgo/ginkgo
script: ginkgo -r --skipMeasurements --cover --trace
env:
    global:
        - PATH=$HOME/gopath/bin:$PATH


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2014 timehop

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.

================================================
FILE: README.md
================================================
# apns

[![GoDoc](https://godoc.org/github.com/timehop/apns?status.svg)](https://godoc.org/github.com/timehop/apns)
[![Build Status](https://travis-ci.org/timehop/apns.svg?branch=master)](https://travis-ci.org/timehop/apns)

A Go package to interface with the Apple Push Notification Service

## Features

This library implements a few features that we couldn't find in any one library elsewhere:

* **Long Lived Clients** - Apple's documentation say that you should hold [a persistent connection open](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW6) and not create new connections for every payload
* **Use of New Protocol** - Apple came out with v2 of their API with support for variable length payloads. This library uses that protocol.
* **Robust Send Guarantees** - APNS has asynchronous feedback on whether a push sent. That means that if you send pushes after a bad send, those pushes will be lost forever. Our library records the last N pushes, detects errors, and is able to resend the pushes that could have been lost. [More reading](http://redth.codes/the-problem-with-apples-push-notification-ser/)

## API Compatibility

The apns package may undergo breaking changes. A tool like [godep](https://github.com/tools/godep) is recommended to vendor the current release.

## Install

```
go get github.com/timehop/apns
```

Checkout the `develop` branch for the current work in progress.

## Usage

### Sending a push notification (basic)

```go
c, _ := apns.NewClient(apns.ProductionGateway, apnsCert, apnsKey)

p := apns.NewPayload()
p.APS.Alert.Body = "I am a push notification!"
p.APS.Badge.Set(5)
p.APS.Sound = "turn_down_for_what.aiff"

m := apns.NewNotification()
m.Payload = p
m.DeviceToken = "A_DEVICE_TOKEN"
m.Priority = apns.PriorityImmediate

c.Send(m)
```

### Sending a push notification with error handling

```go
    c, err := apns.NewClientWithFiles(apns.ProductionGateway, "cert.pem", "key.pem")
    if err != nil {
        log.Fatal("could not create new client", err.Error())
    }

    go func() {
        for f := range c.FailedNotifs {
            fmt.Println("Notif", f.Notif.ID, "failed with", f.Err.Error())
        }
    }()

    p := apns.NewPayload()
    p.APS.Alert.Body = "I am a push notification!"
    p.APS.Badge.Set(5)
    p.APS.Sound = "turn_down_for_what.aiff"
    p.APS.ContentAvailable = 1

    p.SetCustomValue("link", "zombo://dot/com")
    p.SetCustomValue("game", map[string]int{"score": 234})

    m := apns.NewNotification()
    m.Payload = p
    m.DeviceToken = "A_DEVICE_TOKEN"
    m.Priority = apns.PriorityImmediate
    m.Identifier = 12312       // Integer for APNS
    m.ID = "user_id:timestamp" // ID not sent to Apple – to identify error notifications

    c.Send(m)
```

### Retrieving feedback

```go
f, err := apns.NewFeedback(s.Address(), DummyCert, DummyKey)
if err != nil {
	log.Fatal("Could not create feedback", err.Error())
}

for ft := range f.Receive() {
	fmt.Println("Feedback for token:", ft.DeviceToken)
}
```

Note that the channel returned from `Receive` will close after the
[feedback service](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3)
has no more data to send.

## Running the tests

We use [Ginkgo](https://onsi.github.io/ginkgo) for our testing framework and
[Gomega](http://onsi.github.io/gomega/) for our matchers. To run the tests:

```
go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega
ginkgo -randomizeAllSpecs
```

## Contributing

- Fork the repo ([Recommended process](https://splice.com/blog/contributing-open-source-git-repositories-go/))
- Make your changes
- [Run the tests](https://github.com/timehop/apns#running-the-tests)
- Submit a pull request

## License

[MIT License](https://github.com/timehop/apns/blob/master/LICENSE)


================================================
FILE: apns_suite_test.go
================================================
package apns_test

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"

	"testing"
)

func TestApns(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Apns Suite")
}


================================================
FILE: badge_number.go
================================================
package apns

import "encoding/json"

// BadgeNumber is much a NullInt64 in
// database/sql except instead of using
// the nullable value for driver.Value
// encoding and decoding, this is specifically
// meant for JSON encoding and decoding
type BadgeNumber struct {
	Number uint
	IsSet  bool
}

// Unset will reset the BadgeNumber to
// both 0 and invalid
func (b *BadgeNumber) Unset() {
	b.Number = 0
	b.IsSet = false
}

// Set will set the BadgeNumber value to
// the number passed in, assuming it's >= 0.
// If so, the BadgeNumber will also be marked valid
func (b *BadgeNumber) Set(number uint) {
	b.Number = number
	b.IsSet = true
}

// MarshalJSON will marshall the numerical value of
// BadgeNumber
func (b BadgeNumber) MarshalJSON() ([]byte, error) {
	return json.Marshal(b.Number)
}

// UnmarshalJSON will take any non-nil value and
// set BadgeNumber's numeric value to it. It assumes
// that if the unmarshaller gets here, there is a
// number to unmarshal and it's valid
func (b *BadgeNumber) UnmarshalJSON(data []byte) error {
	err := json.Unmarshal(data, &b.Number)
	if err != nil {
		return err
	}

	// Since the point of this type is to
	// allow proper inclusion of 0 for int
	// types while respecting omitempty,
	// assume that set==true if there is
	// a value to unmarshal
	b.IsSet = true
	return nil
}


================================================
FILE: badge_number_test.go
================================================
package apns_test

import (
	"encoding/json"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/timehop/apns"
)

var _ = Describe("BadgeNumber", func() {
	Describe("Defaults", func() {
		It("Should have the proper default values", func() {
			b := apns.BadgeNumber{}
			Expect(b.Number).To(Equal(uint(0)))
			Expect(b.IsSet).To(BeFalse())
		})
	})

	Describe("NewBadgeNumber", func() {
		var b apns.BadgeNumber

		Context("with an argument", func() {
			It("should have values set properly", func() {
				b.Set(5)
				Expect(b.IsSet).To(BeTrue())
				Expect(b.Number).To(Equal(uint(5)))
			})
		})
		Context("when unset", func() {
			It("should reset its values", func() {
				b.Unset()
				Expect(b.IsSet).To(BeFalse())
				Expect(b.Number).To(Equal(uint(0)))
			})
		})
	})

	Describe("JSON handling", func() {
		type BadgeNumbers struct {
			A apns.BadgeNumber `json:"a"`
			B apns.BadgeNumber `json:"b"`
		}

		Context("when marshalling", func() {
			It("should not error", func() {
				bn := apns.BadgeNumber{}
				bn.Set(10)
				_, err := json.Marshal(BadgeNumbers{
					A: bn,
				})
				Expect(err).To(BeNil())
			})
			It("should create the proper values", func() {
				bn := apns.BadgeNumber{}
				bn.Set(10)
				bns := BadgeNumbers{
					A: bn,
				}
				b, _ := json.Marshal(bns)
				expected := "{\"a\":10,\"b\":0}"
				Expect(string(b)).To(Equal(expected))
			})
		})

		Context("when unmarshalled", func() {
			var bnumbers BadgeNumbers

			It("should unmarshal without errors", func() {
				err := json.Unmarshal([]byte("{\"a\":10,\"b\":0}"), &bnumbers)
				Expect(err).To(BeNil())
			})
			It("should populate the struct properly", func() {
				json.Unmarshal([]byte("{\"a\":10,\"b\":0}"), &bnumbers)
				Expect(bnumbers.A.IsSet).To(BeTrue())
				Expect(bnumbers.B.IsSet).To(BeTrue())

				Expect(bnumbers.A.Number).To(Equal(uint(10)))
				Expect(bnumbers.B.Number).To(Equal(uint(0)))
			})
		})
	})
})


================================================
FILE: client.go
================================================
package apns

import (
	"container/list"
	"crypto/tls"
	"io"
	"log"
	"time"
)

type buffer struct {
	size int
	*list.List
}

func newBuffer(size int) *buffer {
	return &buffer{size, list.New()}
}

func (b *buffer) Add(v interface{}) *list.Element {
	e := b.PushBack(v)

	if b.Len() > b.size {
		b.Remove(b.Front())
	}

	return e
}

type Client struct {
	Conn         *Conn
	FailedNotifs chan NotificationResult

	notifs chan Notification
	id     uint32
}

func newClientWithConn(gw string, conn Conn) Client {
	c := Client{
		Conn:         &conn,
		FailedNotifs: make(chan NotificationResult),
		id:           uint32(1),
		notifs:       make(chan Notification),
	}

	go c.runLoop()

	return c
}

func NewClientWithCert(gw string, cert tls.Certificate) Client {
	conn := NewConnWithCert(gw, cert)

	return newClientWithConn(gw, conn)
}

func NewClient(gw string, cert string, key string) (Client, error) {
	conn, err := NewConn(gw, cert, key)
	if err != nil {
		return Client{}, err
	}

	return newClientWithConn(gw, conn), nil
}

func NewClientWithFiles(gw string, certFile string, keyFile string) (Client, error) {
	conn, err := NewConnWithFiles(gw, certFile, keyFile)
	if err != nil {
		return Client{}, err
	}

	return newClientWithConn(gw, conn), nil
}

func (c *Client) Send(n Notification) error {
	c.notifs <- n
	return nil
}

func (c *Client) reportFailedPush(v interface{}, err *Error) {
	failedNotif, ok := v.(Notification)
	if !ok || v == nil {
		return
	}

	select {
	case c.FailedNotifs <- NotificationResult{Notif: failedNotif, Err: *err}:
	default:
	}
}

func (c *Client) requeue(cursor *list.Element) {
	// If `cursor` is not nil, this means there are notifications that
	// need to be delivered (or redelivered)
	for ; cursor != nil; cursor = cursor.Next() {
		if n, ok := cursor.Value.(Notification); ok {
			go func() { c.notifs <- n }()
		}
	}
}

func (c *Client) handleError(err *Error, buffer *buffer) *list.Element {
	cursor := buffer.Back()

	for cursor != nil {
		// Get notification
		n, _ := cursor.Value.(Notification)

		// If the notification, move cursor after the trouble notification
		if n.Identifier == err.Identifier {
			go c.reportFailedPush(cursor.Value, err)

			next := cursor.Next()

			buffer.Remove(cursor)
			return next
		}

		cursor = cursor.Prev()
	}

	return cursor
}

func (c *Client) runLoop() {
	sent := newBuffer(50)
	cursor := sent.Front()

	// APNS connection
	for {
		err := c.Conn.Connect()
		if err != nil {
			// TODO Probably want to exponentially backoff...
			time.Sleep(1 * time.Second)
			continue
		}

		// Start reading errors from APNS
		errs := readErrs(c.Conn)

		c.requeue(cursor)

		// Connection open, listen for notifs and errors
		for {
			var err error
			var n Notification

			// Check for notifications or errors. There is a chance we'll send notifications
			// if we already have an error since `select` will "pseudorandomly" choose a
			// ready channels. It turns out to be fine because the connection will already
			// be closed and it'll requeue. We could check before we get to this select
			// block, but it doesn't seem worth the extra code and complexity.
			select {
			case err = <-errs:
			case n = <-c.notifs:
			}

			// If there is an error we understand, find the notification that failed,
			// move the cursor right after it.
			if nErr, ok := err.(*Error); ok {
				cursor = c.handleError(nErr, sent)
				break
			}

			if err != nil {
				break
			}

			// Add to list
			cursor = sent.Add(n)

			// Set identifier if not specified
			if n.Identifier == 0 {
				n.Identifier = c.id
				c.id++
			} else if c.id < n.Identifier {
				c.id = n.Identifier + 1
			}

			b, err := n.ToBinary()
			if err != nil {
				// TODO
				continue
			}

			_, err = c.Conn.Write(b)

			if err == io.EOF {
				log.Println("EOF trying to write notification")
				break
			}

			if err != nil {
				log.Println("err writing to apns", err.Error())
				break
			}

			cursor = cursor.Next()
		}
	}
}

func readErrs(c *Conn) chan error {
	errs := make(chan error)

	go func() {
		p := make([]byte, 6, 6)
		_, err := c.Read(p)
		if err != nil {
			errs <- err
			return
		}

		e := NewError(p)
		errs <- &e
	}()

	return errs
}


================================================
FILE: client_test.go
================================================
package apns_test

import (
	"bytes"
	"encoding/binary"
	"io/ioutil"
	"os"
	"time"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/timehop/apns"
)

var _ = Describe("Client", func() {
	Describe(".NewConn", func() {
		Context("bad cert/key pair", func() {
			It("should error out", func() {
				_, err := apns.NewClient(apns.ProductionGateway, "missing", "missing_also")
				Expect(err).NotTo(BeNil())
			})
		})

		Context("valid cert/key pair", func() {
			It("should create a valid client", func() {
				c, err := apns.NewClient(apns.ProductionGateway, DummyCert, DummyKey)
				Expect(err).To(BeNil())
				Expect(c.Conn).NotTo(BeNil())
			})
		})
	})

	Describe(".NewConnWithFiles", func() {
		Context("missing cert/key pair", func() {
			It("should error out", func() {
				_, err := apns.NewClientWithFiles(apns.ProductionGateway, "missing", "missing_also")
				Expect(err).NotTo(BeNil())
			})
		})

		Context("valid cert/key pair", func() {
			var certFile, keyFile *os.File

			BeforeEach(func() {
				certFile, _ = ioutil.TempFile("", "cert.pem")
				certFile.Write([]byte(DummyCert))
				certFile.Close()

				keyFile, _ = ioutil.TempFile("", "key.pem")
				keyFile.Write([]byte(DummyKey))
				keyFile.Close()
			})

			AfterEach(func() {
				if certFile != nil {
					os.Remove(certFile.Name())
				}

				if keyFile != nil {
					os.Remove(keyFile.Name())
				}
			})

			It("should create a valid client", func() {
				c, err := apns.NewClientWithFiles(apns.ProductionGateway, certFile.Name(), keyFile.Name())
				Expect(err).To(BeNil())
				Expect(c.Conn).NotTo(BeNil())
			})
		})
	})

	Describe("#Send", func() {
		Context("simple write", func() {
			as := [][]serverAction{
				[]serverAction{
					serverAction{action: readAction, data: []byte{}},
				},
			}

			It("should not return an error", func(d Done) {
				mockDone := make(chan interface{})
				withMockServerAsync(as, mockDone, func(s *mockTLSServer) {
					c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey)
					c.Conn.Conf.InsecureSkipVerify = true

					Expect(c.Send(apns.Notification{})).To(BeNil())

					close(mockDone)
					close(d)
				})
			})
		})

		Context("simple write with buffer", func() {
			as := [][]serverAction{
				[]serverAction{
					serverAction{action: readAction, data: []byte{}},
				},
			}

			It("should not return an error", func(d Done) {
				mockDone := make(chan interface{})
				withMockServerAsync(as, mockDone, func(s *mockTLSServer) {
					c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey)
					c.Conn.Conf.InsecureSkipVerify = true

					for i := 0; i < 54; i++ {
						Expect(c.Send(apns.Notification{})).To(BeNil())
					}

					close(mockDone)
					close(d)
				})
			})
		})

		Context("multiple write", func() {
			as := [][]serverAction{
				[]serverAction{
					serverAction{action: readAction, data: []byte{}},
					serverAction{action: readAction, data: []byte{}},
				},
			}

			It("should not return an error", func(d Done) {
				mockDone := make(chan interface{})
				withMockServerAsync(as, mockDone, func(s *mockTLSServer) {
					c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey)
					c.Conn.Conf.InsecureSkipVerify = true

					Expect(c.Send(apns.Notification{})).To(BeNil())
					Expect(c.Send(apns.Notification{})).To(BeNil())

					close(mockDone)
					close(d)
				})
			})
		})

		Context("bad push", func() {
			n := apns.Notification{Identifier: 9, ID: "some_rando"}
			nb, _ := n.ToBinary()
			nbcb := make([]byte, len(nb))

			errPayload := bytes.NewBuffer([]byte{})
			binary.Write(errPayload, binary.BigEndian, uint8(8))
			binary.Write(errPayload, binary.BigEndian, uint8(8))
			binary.Write(errPayload, binary.BigEndian, uint32(9))

			as := [][]serverAction{
				[]serverAction{
					serverAction{action: readAction, data: []byte{}},
					serverAction{action: readAction, data: nbcb, cb: func(a serverAction) {
						Expect(a.data).To(Equal(nb))
					}},

					// Bad push results in a close
					serverAction{action: writeAction, data: errPayload.Bytes()},
					serverAction{action: closeAction, data: []byte{}},
				},
			}

			It("should not return an error", func(d Done) {
				mockDone := make(chan interface{})
				withMockServerAsync(as, mockDone, func(s *mockTLSServer) {
					c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey)
					c.Conn.Conf.InsecureSkipVerify = true

					go func() {
						n := <-c.FailedNotifs

						Expect(n.Notif.Identifier).To(Equal(uint32(9)))
						Expect(n.Notif.ID).To(Equal("some_rando"))

						close(mockDone)
						close(d)
					}()

					Expect(c.Send(n)).To(BeNil())
				})
			})
		})

		Context("closed, reconnect", func() {
			done := make(chan bool)

			n1 := apns.Notification{Identifier: 1}
			n1b, _ := n1.ToBinary()
			n1bcb := make([]byte, len(n1b))

			errPayload := bytes.NewBuffer([]byte{})
			binary.Write(errPayload, binary.BigEndian, uint8(8))
			binary.Write(errPayload, binary.BigEndian, uint8(8))
			binary.Write(errPayload, binary.BigEndian, uint32(2))

			It("should not return an error", func(d Done) {
				mockDone := make(chan interface{})

				as := [][]serverAction{
					[]serverAction{
						// Write error
						serverAction{action: writeAction, data: errPayload.Bytes(), cb: func(a serverAction) {
							done <- true
						}},

						// Close on error
						serverAction{action: closeAction, cb: func(a serverAction) {
						}},
					},
					[]serverAction{
						// Reconnect
						serverAction{action: readAction, data: []byte{}, cb: func(a serverAction) {
							// Reconnected
						}},

						// Read first good notification
						serverAction{action: readAction, data: n1bcb, cb: func(a serverAction) {
							Expect(a.data).To(Equal(n1b))

							close(mockDone)
							close(d)
						}},
					},
				}

				withMockServerAsync(as, mockDone, func(s *mockTLSServer) {
					c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey)
					c.Conn.Conf.InsecureSkipVerify = true

					<-done
					time.Sleep(5 * time.Millisecond)

					// Good
					Expect(c.Send(n1)).To(BeNil())
				})
			})
		})

		Context("good, close, good, requeue of last good", func() {
			closed := make(chan bool)

			n1 := apns.Notification{Identifier: 1}
			n2 := apns.Notification{Identifier: 2}

			n1b, _ := n1.ToBinary()
			n2b, _ := n2.ToBinary()

			n1bcb := make([]byte, len(n1b))
			n2bcb := make([]byte, len(n2b))

			It("should not return an error", func(d Done) {
				mockDone := make(chan interface{})
				as := [][]serverAction{
					[]serverAction{
						// Connect
						serverAction{action: readAction, data: []byte{}, cb: func(a serverAction) {
							// Handshake
						}},

						// Read first good notification
						serverAction{action: readAction, data: n1bcb, cb: func(a serverAction) {
							Expect(a.data).To(Equal(n1b))
						}},

						// Close on error
						serverAction{action: closeAction, cb: func(a serverAction) {
							closed <- true
						}},
					},
					[]serverAction{
						// Reconnect
						serverAction{action: readAction, data: []byte{}, cb: func(a serverAction) {
							// Reconnected
						}},

						// Requeue
						serverAction{action: readAction, data: n2bcb, cb: func(a serverAction) {
							Expect(a.data).To(Equal(n2b))

							close(mockDone)
							close(d)
						}},
					},
				}

				withMockServerAsync(as, mockDone, func(s *mockTLSServer) {
					c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey)
					c.Conn.Conf.InsecureSkipVerify = true

					// Good
					Expect(c.Send(n1)).To(BeNil())

					<-closed
					time.Sleep(5 * time.Millisecond)

					// Good
					Expect(c.Send(n2)).To(BeNil())
				})
			})
		})

		Context("good, bad, good, requeue of last good", func() {
			It("should not return an error", func(d Done) {
				mockDone := make(chan interface{})

				n1 := apns.Notification{Identifier: 1}
				n2 := apns.Notification{Identifier: 2}
				n3 := apns.Notification{Identifier: 3}

				n1b, _ := n1.ToBinary()
				n2b, _ := n2.ToBinary()
				n3b, _ := n3.ToBinary()

				n1bcb := make([]byte, len(n1b))
				n2bcb := make([]byte, len(n2b))
				n3bcb := make([]byte, len(n3b))

				errPayload := bytes.NewBuffer([]byte{})
				binary.Write(errPayload, binary.BigEndian, uint8(8))
				binary.Write(errPayload, binary.BigEndian, uint8(8))
				binary.Write(errPayload, binary.BigEndian, uint32(2))

				as := [][]serverAction{
					[]serverAction{
						// Connect
						serverAction{action: readAction, data: []byte{}, cb: func(a serverAction) {
							// Handshake
						}},

						// Read first good notification
						serverAction{action: readAction, data: n1bcb, cb: func(a serverAction) {
							Expect(a.data).To(Equal(n1b))
						}},

						// Read bad notification
						serverAction{action: readAction, data: n2bcb, cb: func(a serverAction) {
							Expect(a.data).To(Equal(n2b))
						}},

						// Read second good notification
						serverAction{action: readAction, data: n3bcb, cb: func(a serverAction) {
							Expect(a.data).To(Equal(n3b))
						}},

						// Write error
						serverAction{action: writeAction, data: errPayload.Bytes(), cb: func(a serverAction) {
						}},

						// Close on error
						serverAction{action: closeAction, cb: func(a serverAction) {
						}},
					},
					[]serverAction{
						// Reconnect
						serverAction{action: readAction, data: []byte{}, cb: func(a serverAction) {
							// Reconnected
						}},

						// Requeue
						serverAction{action: readAction, data: n3bcb, cb: func(a serverAction) {
							Expect(a.data).To(Equal(n3b))

							close(mockDone)
							close(d)
						}},
					},
				}

				withMockServerAsync(as, mockDone, func(s *mockTLSServer) {
					c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey)
					c.Conn.Conf.InsecureSkipVerify = true

					// Good
					Expect(c.Send(n1)).To(BeNil())

					// Bad
					Expect(c.Send(n2)).To(BeNil())

					// Good
					Expect(c.Send(n3)).To(BeNil())
				})
			})
		})
	})
})


================================================
FILE: conn.go
================================================
package apns

import (
	"crypto/tls"
	"net"
	"strings"
)

const (
	ProductionGateway = "gateway.push.apple.com:2195"
	SandboxGateway    = "gateway.sandbox.push.apple.com:2195"

	ProductionFeedbackGateway = "feedback.push.apple.com:2196"
	SandboxFeedbackGateway    = "feedback.sandbox.push.apple.com:2196"
)

// Conn is a wrapper for the actual TLS connections made to Apple
type Conn struct {
	NetConn net.Conn
	Conf    *tls.Config

	gateway   string
	connected bool
}

func NewConnWithCert(gw string, cert tls.Certificate) Conn {
	gatewayParts := strings.Split(gw, ":")
	conf := tls.Config{
		Certificates: []tls.Certificate{cert},
		ServerName:   gatewayParts[0],
	}

	return Conn{gateway: gw, Conf: &conf}
}

// NewConnWithFiles creates a new Conn from certificate and key in the specified files
func NewConn(gw string, crt string, key string) (Conn, error) {
	cert, err := tls.X509KeyPair([]byte(crt), []byte(key))
	if err != nil {
		return Conn{}, err
	}

	return NewConnWithCert(gw, cert), nil
}

// NewConnWithFiles creates a new Conn from certificate and key in the specified files
func NewConnWithFiles(gw string, certFile string, keyFile string) (Conn, error) {
	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return Conn{}, err
	}

	return NewConnWithCert(gw, cert), nil
}

// Connect actually creates the TLS connection
func (c *Conn) Connect() error {
	// Make sure the existing connection is closed
	if c.NetConn != nil {
		c.NetConn.Close()
	}

	conn, err := net.Dial("tcp", c.gateway)
	if err != nil {
		return err
	}

	tlsConn := tls.Client(conn, c.Conf)
	err = tlsConn.Handshake()
	if err != nil {
		return err
	}

	c.NetConn = tlsConn
	return nil
}

func (c *Conn) Close() error {
	if c.NetConn != nil {
		return c.NetConn.Close()
	}

	return nil
}

// Read reads data from the connection
func (c *Conn) Read(p []byte) (int, error) {
	i, err := c.NetConn.Read(p)
	return i, err
}

// Write writes data from the connection
func (c *Conn) Write(p []byte) (int, error) {
	return c.NetConn.Write(p)
}


================================================
FILE: conn_test.go
================================================
package apns_test

import (
	"bytes"
	"crypto/tls"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"os"
	"strings"
	"time"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/timehop/apns"
)

var DummyCert = `-----BEGIN CERTIFICATE-----
MIIC9TCCAd+gAwIBAgIQf3bEgFWUb+q6eK5ySkV/gjALBgkqhkiG9w0BAQUwEjEQ
MA4GA1UEChMHQWNtZSBDbzAeFw0xNDA2MzAwNDI5MDhaFw0xNTA2MzAwNDI5MDha
MBIxEDAOBgNVBAoTB0FjbWUgQ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQDhAgWrrFZBtCfVEPg1tSIr9fuSUoeundb556IUr9uOmOHaYK7r3/I43acw
bVIfaenFxwUUf8YakQzTjOa5qSfK/Eylyw2ezBJtNUEqcHw0f+y66+jJbZa4clPa
tL6ezaMS/syXPpvNU8+16jdVdTJzqdBdSGAZMOCeumUWDNdlfBmHPVq1JMy0uGmO
XDoZK2Ir0/3LUfjk9R2wdm1VLrJAml7F0L0FhBHHXgHOSFM2ixjGflffaiuTCxhW
1z1NTo9XjWUQh2iM9Udf+xVnJLGLZ0EMFr2qihuK604Fp4SlNHEF+UWUn+j0PYo+
LbzM9oKJcdVD0XI36vrn3rGPHO9vAgMBAAGjSzBJMA4GA1UdDwEB/wQEAwIAoDAT
BgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCWxv
Y2FsaG9zdDALBgkqhkiG9w0BAQUDggEBAGJ/3I4KKlbEwLAC5ut4ZZ9V8WF4sHkI
Lj7e4vx2pPi6hf9miV1ff01NrpfUna7flwL9yD7Ybl7jRRIB4rIcKk+U5djGsT3H
ScGkbIMKrr08drWw1g4JU6PBH7xTfzGxNRERrnmrbJV0jCo9Tt8i53IpPtp6Z2Q1
8ydtPhU+Bpe2YoNr1w1fSV1JHXqjKV8RlGkCNSi4ozPOO8RbAYnBT3d9XSGoX//q
RGJUf3wC/rCxJkN63Moxuy3vxV2TmiqccHOrXJSJ8P/4PpPV/xuBk5k4HS1Nfmew
d9WHHn6bMJE9arVvWAiu9teCadVffuS2cl2cicN4XB6Ui0aDqhG2Exw=
-----END CERTIFICATE-----`

var DummyKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA4QIFq6xWQbQn1RD4NbUiK/X7klKHrp3W+eeiFK/bjpjh2mCu
69/yON2nMG1SH2npxccFFH/GGpEM04zmuaknyvxMpcsNnswSbTVBKnB8NH/suuvo
yW2WuHJT2rS+ns2jEv7Mlz6bzVPPteo3VXUyc6nQXUhgGTDgnrplFgzXZXwZhz1a
tSTMtLhpjlw6GStiK9P9y1H45PUdsHZtVS6yQJpexdC9BYQRx14BzkhTNosYxn5X
32orkwsYVtc9TU6PV41lEIdojPVHX/sVZySxi2dBDBa9qoobiutOBaeEpTRxBflF
lJ/o9D2KPi28zPaCiXHVQ9FyN+r6596xjxzvbwIDAQABAoIBAFzW+cIA5MJNdFX8
n32BlGzxHPEd7nAFHmuUwJKqkPwAZsg1NleK2qXOByr7IHRnvhZl7Nmtcu8JRHKR
Y63ddtbRTUrnQmJwL3YyEAZTzVvYILRrnGxoNFU8jw7hnvllPdEbow0QvzZ0S3Lz
BgvTxJJm0dt7fnNGcJftrsHvYHy1dptaR4hPv0xV5G7RPrbTl94llKfi745tp5Wd
xGpnjcBXoAnzCVRij1tHfSYubRJ2MJV0kzG3oVdRV2P/zWaout8BlhLCURv4sRUX
7FfCNa/z+G6AlROjCKJUP9YIUbxBEa/aP8YlSiyLRi1jFbMWcnKWQUdqS19m73Ap
a1LJFPECgYEA+Ve5DegcrWnUb2HsHD38HlmEg6S+/jg2P4TsuLZBtvO4/vzRx/qq
pwuuMm2CsvXr4nVmMEsMlSzYdsnaXIlWqyVDCOwIWR5VYT2GDWqQLaIXPlFaISzN
27tHd64KUtR1fMJUwQVK/MUORUbpYoAnSIil2SlYkWUhF024fNP8CxcCgYEA5wP4
HLiqU2rqe7vSAF/8fHwPleTzuCfMCVZm0aegUzQQQtklZoVE/BBwEGHdXflq1veq
pHeC8bNR4BF6ZgeSWgbLVF3msquy47QeNElHA2muJd3qmNWz4LXo1Pxb8KXcnXri
QZ+r3Y8obWTFQYq7gGQGPLXGTV3bhLGIyrT4lWkCgYAgZ2MYSJL5gmhmNT6fCPsr
4oxTI2Ti2uFJ7fdppd3ybcgb8zU8HPpyjRUNXqf+o/EM1B78pbQz6skS3vau0fZe
dZA5p5sKIeQMqBc0xSWJmKgWpDHnX9A8/yCxj/+tdgjytrqW/x4YrW9GV4nbEDaK
uZ98EmB9PLxJMAOKzW3S7wKBgQDD4PCy4b3CR2iVC9dva/P5VXQdo+knX884p6M8
58YgZofXNqnouN2aYRG0QlbiBMcbiRqOo6tK58JnnEpNUuQ8I4Cqg4hGPSHMwv/N
U8i70xLPltABUUpZIcVPOr92WBytBvHrtMiUb3tW7lf3T/vWTHmhZnvDQ+8LH0Ge
pz4T6QKBgQCoBJKOd781IQmT6i5hHSYJlsP6ymaaaQniJPVpnci/jf8+2QtponQY
scgnaBLBasLQ6GfKSRtcyidEi9wwxpVj0tw2p567jeNcIveD0TOYFf0RHEfrs+D4
VdRgai/v2NbFZLDnzeGVuYypXu6R78isJfHtz/a0aEave8yB3CRiDw==
-----END RSA PRIVATE KEY-----`

// To be able to run in parallel
var mockPort = 50000

// Mock Addr
type mockAddr struct {
}

func (m mockAddr) Network() string {
	return "localhost:56789"
}

func (m mockAddr) String() string {
	return "localhost:56789"
}

// Mock TLS connection
type mockTLSNetConn struct {
	bb  *bytes.Buffer
	err error
}

func (t mockTLSNetConn) Read(p []byte) (int, error) {
	r := bytes.NewReader(t.bb.Bytes())
	return r.Read(p)
}

func (t mockTLSNetConn) Write(p []byte) (int, error) {
	return t.bb.Write(p)
}

func (t mockTLSNetConn) Close() error {
	return t.err
}

func (m mockTLSNetConn) LocalAddr() net.Addr {
	return mockAddr{}
}

func (m mockTLSNetConn) RemoteAddr() net.Addr {
	return mockAddr{}
}

func (m mockTLSNetConn) SetDeadline(t time.Time) error {
	return nil
}

func (m mockTLSNetConn) SetReadDeadline(t time.Time) error {
	return nil
}

func (m mockTLSNetConn) SetWriteDeadline(t time.Time) error {
	return nil
}

type serverAction struct {
	action string
	data   []byte
	cb     func(s serverAction)
}

const (
	readAction  = "read"
	writeAction = "write"
	closeAction = "close"
)

type mockTLSServer struct {
	Port                   int
	Server                 net.Listener
	ConnectionActionGroups [][]serverAction
}

func (m *mockTLSServer) portStr() string {
	if m.Port == 0 {
		mockPort = mockPort + 1
		m.Port = mockPort
	}

	return fmt.Sprint(m.Port)
}

func (m *mockTLSServer) Address() string {
	return "localhost:" + m.portStr()
}

func (m *mockTLSServer) start() {
	cert, err := tls.X509KeyPair([]byte(DummyCert), []byte(DummyKey))
	if err != nil {
		log.Panic(err)
	}

	config := tls.Config{Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAnyClientCert}

	m.Server, err = tls.Listen("tcp", "localhost:"+m.portStr(), &config)
	go func() {
		for i := 0; i < len(m.ConnectionActionGroups); i++ {
			g := m.ConnectionActionGroups[i]

			// Wait for a connection.
			conn, err := m.Server.Accept()
			if err != nil {
				if strings.Contains(err.Error(), "use of closed network connection") {
					return
				} else {
					log.Fatal(err)
				}
			}
			// Handle the connection in a new goroutine.
			// The loop then returns to accepting, so that
			// multiple connections may be served concurrently.
			go func(c net.Conn) {
				for j := 0; j < len(g); j++ {
					a := g[j]
					switch a.action {
					case readAction:
						c.Read(a.data)
					case writeAction:
						c.Write(a.data)
					case closeAction:
						c.Close()

						if a.cb != nil {
							a.cb(a)
						}
						return
					}

					if a.cb != nil {
						a.cb(a)
					}
				}
			}(conn)
		}

		// No more connection action groups
	}()
}

func (m *mockTLSServer) stop() {
	if m.Server != nil {
		m.Server.Close()
	}
}

var withMockServer = func(as [][]serverAction, cb func(s *mockTLSServer)) {
	d := make(chan interface{})
	withMockServerAsync(as, d, func(s *mockTLSServer) {
		cb(s)
		close(d)
	})
}

var withMockServerAsync = func(as [][]serverAction, d chan interface{}, cb func(s *mockTLSServer)) {
	s := &mockTLSServer{}
	s.ConnectionActionGroups = as

	s.start()

	cb(s)

	<-d
	s.stop()
}

// Tests
var _ = Describe("Conn", func() {
	Describe(".NewConn", func() {
		Context("bad key/cert pair", func() {
			It("should return an error", func() {
				_, err := apns.NewConn(apns.SandboxGateway, "missing", "missing")
				Expect(err).NotTo(BeNil())
			})
		})

		Context("valid key/cert pair", func() {
			It("should not return an error", func() {
				_, err := apns.NewConn(apns.SandboxGateway, DummyCert, DummyKey)
				Expect(err).To(BeNil())
			})
		})
	})

	Describe(".NewConnWithFiles", func() {
		Context("missing files", func() {
			It("should return an error", func() {
				_, err := apns.NewConnWithFiles(apns.SandboxGateway, "missing.pem", "missing.pem")
				Expect(err).NotTo(BeNil())
			})
		})

		Context("with valid cert/key pair", func() {
			var certFile, keyFile *os.File
			var err error

			BeforeEach(func() {
				certFile, _ = ioutil.TempFile("", "cert.pem")
				certFile.Write([]byte(DummyCert))
				certFile.Close()

				keyFile, _ = ioutil.TempFile("", "key.pem")
				keyFile.Write([]byte(DummyKey))
				keyFile.Close()
			})

			AfterEach(func() {
				if certFile != nil {
					os.Remove(certFile.Name())
				}

				if keyFile != nil {
					os.Remove(keyFile.Name())
				}
			})

			It("should returning a connection", func() {
				_, err = apns.NewConnWithFiles(apns.SandboxGateway, certFile.Name(), keyFile.Name())
				Expect(err).To(BeNil())
			})
		})
	})

	Describe("#Connect()", func() {
		Context("server not up", func() {
			conn, _ := apns.NewConnWithFiles(apns.SandboxGateway, "missing.pem", "missing.pem")

			It("should return an error", func() {
				err := conn.Connect()
				Expect(err).NotTo(BeNil())
			})
		})

		Context("server up", func() {
			as := [][]serverAction{[]serverAction{serverAction{action: readAction, data: []byte{}}}}

			Context("with untrusted certs", func() {
				It("should return an error", func(d Done) {
					withMockServer(as, func(s *mockTLSServer) {
						conn, _ := apns.NewConn(s.Address(), DummyCert, DummyKey)
						err := conn.Connect()
						Expect(err).NotTo(BeNil())

						close(d)
					})
				})
			})

			Context("trusting the certs", func() {
				It("should not return an error", func(d Done) {
					withMockServer(as, func(s *mockTLSServer) {
						conn, _ := apns.NewConn(s.Address(), DummyCert, DummyKey)
						conn.Conf.InsecureSkipVerify = true

						err := conn.Connect()
						Expect(err).To(BeNil())

						close(d)
					})
				})
			})

			Context("with existing connection", func() {
				It("should not return an error", func(d Done) {
					as = [][]serverAction{
						[]serverAction{serverAction{action: readAction, data: []byte{}}},
						[]serverAction{serverAction{action: readAction, data: []byte{}}},
					}

					withMockServer(as, func(s *mockTLSServer) {
						conn, _ := apns.NewConn(s.Address(), DummyCert, DummyKey)
						conn.Conf.InsecureSkipVerify = true

						conn.Connect()

						err := conn.Connect()
						Expect(err).To(BeNil())

						close(d)
					})
				})
			})
		})
	})

	Describe("#Read", func() {
		rwc := mockTLSNetConn{bb: bytes.NewBuffer([]byte("hello!"))}

		pp := make([]byte, 6)
		bytes.NewReader(rwc.bb.Bytes()).Read(pp)

		conn, _ := apns.NewConn(apns.ProductionGateway, DummyCert, DummyKey)
		conn.NetConn = rwc

		It("should read out 'hello!'", func() {
			p := make([]byte, 6)
			conn.Read(p)

			Expect(p).To(Equal([]byte("hello!")))
		})
	})

	Describe("#Write", func() {
		rwc := mockTLSNetConn{bb: bytes.NewBuffer([]byte{})}

		conn, _ := apns.NewConn(apns.ProductionGateway, DummyCert, DummyKey)
		conn.NetConn = rwc

		It("should write out 'world!'", func() {
			conn.Write([]byte("world!"))
			Expect(rwc.bb.String()).To(Equal("world!"))
		})
	})

	Describe("#Close", func() {
		Context("with connection", func() {
			Context("no error", func() {
				rwc := mockTLSNetConn{bb: bytes.NewBuffer([]byte{})}

				conn, _ := apns.NewConn(apns.ProductionGateway, DummyCert, DummyKey)
				conn.NetConn = rwc

				It("should return no error", func() {
					Expect(rwc.Close()).To(BeNil())
				})
			})

			Context("with error", func() {
				rwc := mockTLSNetConn{bb: bytes.NewBuffer([]byte{})}

				conn, _ := apns.NewConn(apns.ProductionGateway, DummyCert, DummyKey)
				conn.NetConn = rwc

				rwc.err = io.EOF
				It("should return that error", func() {
					Expect(rwc.Close()).To(Equal(io.EOF))
				})
			})
		})

		Context("without connection", func() {
			c, _ := apns.NewConn(apns.ProductionGateway, DummyCert, DummyKey)
			It("should not return an error", func() {
				Expect(c.Close()).To(BeNil())
			})
		})
	})
})


================================================
FILE: doc.go
================================================
/*
A Go package to interface with the Apple Push
Notification Service

Features

This library implements a few features that we couldn't find in any one
library elsewhere:

  Long Lived Clients     - Apple's documentation say that you should hold a
                           persistent connection open and not create new
                           connections for every payload
                           See: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW6)

  Use of New Protocol    - Apple came out with v2 of their API with support for
                           variable length payloads. This library uses that
                           protocol.

  Robust Send Guarantees - APNS has asynchronous feedback on whether a push
                           sent. That means that if you send pushes after a
                           bad send, those pushes will be lost forever. Our
                           library records the last N pushes, detects errors,
                           and is able to resend the pushes that could have
                           been lost.
                           See: http://redth.codes/the-problem-with-apples-push-notification-ser/
*/
package apns


================================================
FILE: error.go
================================================
package apns

import (
	"bytes"
	"encoding/binary"
)

const (
	// Error strings based on the codes specified here:
	// https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW12
	ErrProcessing         = "Processing error"
	ErrMissingDeviceToken = "Missing device token"
	ErrMissingTopic       = "Missing topic"
	ErrMissingPayload     = "Missing payload"
	ErrInvalidTokenSize   = "Invalid token size"
	ErrInvalidTopicSize   = "Invalid topic size"
	ErrInvalidPayloadSize = "Invalid payload size"
	ErrInvalidToken       = "Invalid token"
	ErrShutdown           = "Shutdown"
	ErrUnknown            = "None (unknown)"
)

var errorMapping = map[uint8]string{
	1:   ErrProcessing,
	2:   ErrMissingDeviceToken,
	3:   ErrMissingTopic,
	4:   ErrMissingPayload,
	5:   ErrInvalidTokenSize,
	6:   ErrInvalidTopicSize,
	7:   ErrInvalidPayloadSize,
	8:   ErrInvalidToken,
	10:  ErrShutdown,
	255: ErrUnknown,
}

type Error struct {
	Command    uint8
	Status     uint8
	Identifier uint32
	ErrStr     string
}

func NewError(p []byte) Error {
	if len(p) != 1+1+4 {
		return Error{ErrStr: ErrUnknown}
	}

	r := bytes.NewBuffer(p)
	e := Error{}

	binary.Read(r, binary.BigEndian, &e.Command)
	binary.Read(r, binary.BigEndian, &e.Status)
	binary.Read(r, binary.BigEndian, &e.Identifier)

	var ok bool
	if e.ErrStr, ok = errorMapping[e.Status]; !ok {
		e.ErrStr = ErrUnknown
	}

	return e
}

func (e *Error) Error() string {
	return e.ErrStr
}


================================================
FILE: error_test.go
================================================
package apns_test

import (
	"bytes"
	"encoding/binary"
	"math/rand"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/timehop/apns"
)

var _ = Describe("Error", func() {
	Describe(".NewError", func() {
		ShouldBeErrorWithErrStr := func(status int, errStr string) {
			var errPayload = func(command int, status int, identifier int) []byte {
				buffer := bytes.NewBuffer([]byte{})
				binary.Write(buffer, binary.BigEndian, uint8(command))
				binary.Write(buffer, binary.BigEndian, uint8(status))
				binary.Write(buffer, binary.BigEndian, uint32(identifier))
				return buffer.Bytes()
			}

			command := rand.Int()
			identifier := rand.Int()

			p := errPayload(command, status, identifier)
			e := apns.NewError(p)

			It("should parse the field out correctly", func() {
				Expect(e.Command).To(Equal(uint8(command)))
				Expect(e.Status).To(Equal(uint8(status)))
				Expect(e.Identifier).To(Equal(uint32(identifier)))
			})

			It("should have picked the right error string", func() {
				Expect(e.ErrStr).To(Equal(errStr))
			})
		}

		Context("processing error", func() {
			ShouldBeErrorWithErrStr(1, apns.ErrProcessing)
		})

		Context("device token error", func() {
			ShouldBeErrorWithErrStr(2, apns.ErrMissingDeviceToken)
		})

		Context("missing topic error", func() {
			ShouldBeErrorWithErrStr(3, apns.ErrMissingTopic)
		})

		Context("missing payload error", func() {
			ShouldBeErrorWithErrStr(4, apns.ErrMissingPayload)
		})

		Context("invalid token size error", func() {
			ShouldBeErrorWithErrStr(5, apns.ErrInvalidTokenSize)
		})

		Context("invalid topic size error", func() {
			ShouldBeErrorWithErrStr(6, apns.ErrInvalidTopicSize)
		})

		Context("invalid payload size error", func() {
			ShouldBeErrorWithErrStr(7, apns.ErrInvalidPayloadSize)
		})

		Context("invalid token error", func() {
			ShouldBeErrorWithErrStr(8, apns.ErrInvalidToken)
		})

		Context("shutdown error", func() {
			ShouldBeErrorWithErrStr(10, apns.ErrShutdown)
		})

		Context("unknown error", func() {
			ShouldBeErrorWithErrStr(255, apns.ErrUnknown)
		})

		Context("error with unrecognized code", func() {
			ShouldBeErrorWithErrStr(300, apns.ErrUnknown)
		})

		Context("not enough bytes", func() {
			It("should be ErrUnknown", func() {
				e := apns.NewError([]byte{})
				Expect(e).NotTo(BeNil())
				Expect(e.ErrStr).To(Equal(apns.ErrUnknown))
			})
		})
	})

	Describe("#Error", func() {
		It("should have an error string", func() {
			e := apns.Error{ErrStr: "this is an error string"}
			Expect(e.Error()).To(Equal("this is an error string"))
		})
	})
})


================================================
FILE: example/example.go
================================================
package main

import (
	"fmt"
	"log"

	"github.com/timehop/apns"
)

func main() {
	c, err := apns.NewClientWithFiles(apns.ProductionGateway, "apns.crt", "apns.key")
	if err != nil {
		log.Fatal("Could not create client", err.Error())
	}

	i := 0
	for {
		fmt.Print("Enter '<token> <badge> <msg>': ")

		var tok, body string
		var badge uint

		_, err := fmt.Scanf("%s %d %s", &tok, &badge, &body)
		if err != nil {
			fmt.Printf("Something went wrong: %v\n", err.Error())
			continue
		}

		p := apns.NewPayload()
		p.APS.Alert.Body = body
		p.APS.Badge.Set(badge)

		p.SetCustomValue("link", "yourapp://precache/20140718")

		m := apns.NewNotification()
		m.Payload = p
		m.DeviceToken = tok
		m.Priority = apns.PriorityImmediate
		m.Identifier = uint32(i)

		c.Send(m)

		i++
	}
}


================================================
FILE: feedback.go
================================================
package apns

import (
	"bytes"
	"crypto/tls"
	"encoding/binary"
	"encoding/hex"
	"time"
)

type Feedback struct {
	Conn *Conn
}

type FeedbackTuple struct {
	Timestamp   time.Time
	TokenLength uint16
	DeviceToken string
}

func feedbackTupleFromBytes(b []byte) FeedbackTuple {
	r := bytes.NewReader(b)

	var ts uint32
	binary.Read(r, binary.BigEndian, &ts)

	var tokLen uint16
	binary.Read(r, binary.BigEndian, &tokLen)

	tok := make([]byte, tokLen)
	binary.Read(r, binary.BigEndian, &tok)

	return FeedbackTuple{
		Timestamp:   time.Unix(int64(ts), 0),
		TokenLength: tokLen,
		DeviceToken: hex.EncodeToString(tok),
	}
}

func NewFeedbackWithCert(gw string, cert tls.Certificate) Feedback {
	conn := NewConnWithCert(gw, cert)

	return Feedback{Conn: &conn}
}

func NewFeedback(gw string, cert string, key string) (Feedback, error) {
	conn, err := NewConn(gw, cert, key)
	if err != nil {
		return Feedback{}, err
	}

	return Feedback{Conn: &conn}, nil
}

func NewFeedbackWithFiles(gw string, certFile string, keyFile string) (Feedback, error) {
	conn, err := NewConnWithFiles(gw, certFile, keyFile)
	if err != nil {
		return Feedback{}, err
	}

	return Feedback{Conn: &conn}, nil
}

// Receive returns a read only channel for APNs feedback. The returned channel
// will close when there is no more data to be read.
func (f Feedback) Receive() <-chan FeedbackTuple {
	fc := make(chan FeedbackTuple)
	go f.receive(fc)
	return fc
}

func (f Feedback) receive(fc chan FeedbackTuple) {
	err := f.Conn.Connect()
	if err != nil {
		close(fc)
		return
	}
	defer f.Conn.Close()

	for {
		b := make([]byte, 38)

		f.Conn.NetConn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))

		_, err := f.Conn.Read(b)
		if err != nil {
			close(fc)
			return
		}

		fc <- feedbackTupleFromBytes(b)
	}
}


================================================
FILE: feedback_test.go
================================================
package apns_test

import (
	"bytes"
	"encoding/binary"
	"encoding/hex"
	"io/ioutil"
	"os"
	"time"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/timehop/apns"
)

var _ = Describe("Feedback", func() {
	Describe(".NewFeedback", func() {
		Context("bad cert/key pair", func() {
			It("should error out", func() {
				_, err := apns.NewFeedback(apns.ProductionGateway, "missing", "missing_also")
				Expect(err).NotTo(BeNil())
			})
		})

		Context("valid cert/key pair", func() {
			It("should create a valid client", func() {
				_, err := apns.NewFeedback(apns.ProductionGateway, DummyCert, DummyKey)
				Expect(err).To(BeNil())
			})
		})
	})

	Describe(".NewFeedbackWithFiles", func() {
		Context("missing cert/key pair", func() {
			It("should error out", func() {
				_, err := apns.NewFeedbackWithFiles(apns.ProductionGateway, "missing", "missing_also")
				Expect(err).NotTo(BeNil())
			})
		})

		Context("valid cert/key pair", func() {
			var certFile, keyFile *os.File

			BeforeEach(func() {
				certFile, _ = ioutil.TempFile("", "cert.pem")
				certFile.Write([]byte(DummyCert))
				certFile.Close()

				keyFile, _ = ioutil.TempFile("", "key.pem")
				keyFile.Write([]byte(DummyKey))
				keyFile.Close()
			})

			AfterEach(func() {
				if certFile != nil {
					os.Remove(certFile.Name())
				}

				if keyFile != nil {
					os.Remove(keyFile.Name())
				}
			})

			It("should create a valid client", func() {
				_, err := apns.NewFeedbackWithFiles(apns.ProductionGateway, certFile.Name(), keyFile.Name())
				Expect(err).To(BeNil())
			})
		})
	})

	Describe("#Receive", func() {
		Context("could not connect", func() {
			It("should not receive anything", func() {
				s := &mockTLSServer{}

				f, _ := apns.NewFeedback(s.Address(), DummyCert, DummyKey)
				f.Conn.Conf.InsecureSkipVerify = true

				c := f.Receive()

				r := 0
				for _ = range c {
					r += 1
				}

				Expect(r).To(Equal(0))
			})
		})

		Context("times out", func() {
			as := [][]serverAction{
				[]serverAction{
					serverAction{action: readAction, data: []byte{}},
				},
			}

			withMockServer(as, func(s *mockTLSServer) {
				f, _ := apns.NewFeedback(s.Address(), DummyCert, DummyKey)
				f.Conn.Conf.InsecureSkipVerify = true

				It("should not receive anything", func() {
					c := f.Receive()

					r := 0
					for _ = range c {
						r += 1
					}

					Expect(r).To(Equal(0))
				})
			})
		})

		Context("with feedback", func() {
			f1 := bytes.NewBuffer([]byte{})
			f2 := bytes.NewBuffer([]byte{})
			f3 := bytes.NewBuffer([]byte{})

			// The final token strings
			t1 := "00a18269661e9406aea59a5620b05c7c0e371574fa6f251951de8d7a5a292535"
			t2 := "00a1a4b7294fcfbc5293f63d4298fcecd9c20a893befd45adceead5fc92d3319"
			t3 := "00a1b7893d5e85eb8bb7bf0846b464d075248555118ae893b06e96cfb8d678e3"

			bt1, _ := hex.DecodeString(t1)
			bt2, _ := hex.DecodeString(t2)
			bt3, _ := hex.DecodeString(t3)

			binary.Write(f1, binary.BigEndian, uint32(1404358249))
			binary.Write(f1, binary.BigEndian, uint16(len(bt1)))
			binary.Write(f1, binary.BigEndian, bt1)

			binary.Write(f2, binary.BigEndian, uint32(1404352249))
			binary.Write(f2, binary.BigEndian, uint16(len(bt2)))
			binary.Write(f2, binary.BigEndian, bt2)

			binary.Write(f3, binary.BigEndian, uint32(1394352249))
			binary.Write(f3, binary.BigEndian, uint16(len(bt3)))
			binary.Write(f3, binary.BigEndian, bt3)

			as := [][]serverAction{
				[]serverAction{
					serverAction{action: writeAction, data: f1.Bytes()},
					serverAction{action: writeAction, data: f2.Bytes()},
					serverAction{action: writeAction, data: f3.Bytes()},
				},
			}

			It("should receive feedback", func(d Done) {
				withMockServer(as, func(s *mockTLSServer) {
					f, _ := apns.NewFeedback(s.Address(), DummyCert, DummyKey)
					f.Conn.Conf.InsecureSkipVerify = true

					c := f.Receive()

					r1 := <-c
					Expect(r1.Timestamp).To(Equal(time.Unix(1404358249, 0)))
					Expect(r1.TokenLength).To(Equal(uint16(len(bt1))))
					Expect(r1.DeviceToken).To(Equal(t1))

					r2 := <-c
					Expect(r2.Timestamp).To(Equal(time.Unix(1404352249, 0)))
					Expect(r2.TokenLength).To(Equal(uint16(len(bt2))))
					Expect(r2.DeviceToken).To(Equal(t2))

					r3 := <-c
					Expect(r3.Timestamp).To(Equal(time.Unix(1394352249, 0)))
					Expect(r3.TokenLength).To(Equal(uint16(len(bt3))))
					Expect(r3.DeviceToken).To(Equal(t3))

					<-c
					close(d)
				})
			})
		})
	})
})


================================================
FILE: go.mod
================================================
module github.com/timehop/apns

go 1.20

require (
	github.com/onsi/ginkgo v1.16.5
	github.com/onsi/gomega v1.27.6
)

require (
	github.com/fsnotify/fsnotify v1.4.9 // indirect
	github.com/google/go-cmp v0.5.9 // indirect
	github.com/nxadm/tail v1.4.8 // indirect
	golang.org/x/net v0.8.0 // indirect
	golang.org/x/sys v0.6.0 // indirect
	golang.org/x/text v0.8.0 // indirect
	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: notification.go
================================================
package apns

import (
	"bytes"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"time"
)

const (
	PriorityImmediate     = 10
	PriorityPowerConserve = 5
)

const (
	commandID = 2

	// Items IDs
	deviceTokenItemID            = 1
	payloadItemID                = 2
	notificationIdentifierItemID = 3
	expirationDateItemID         = 4
	priorityItemID               = 5

	// Item lengths
	deviceTokenItemLength            = 32
	notificationIdentifierItemLength = 4
	expirationDateItemLength         = 4
	priorityItemLength               = 1
)

type NotificationResult struct {
	Notif Notification
	Err   Error
}

type Alert struct {
	// Do not add fields without updating the implementation of isZero.
	Body         string   `json:"body,omitempty"`
	Title        string   `json:"title,omitempty"`
	Action       string   `json:"action,omitempty"`
	LocKey       string   `json:"loc-key,omitempty"`
	LocArgs      []string `json:"loc-args,omitempty"`
	ActionLocKey string   `json:"action-loc-key,omitempty"`
	LaunchImage  string   `json:"launch-image,omitempty"`
}

func (a *Alert) isSimple() bool {
	return len(a.Title) == 0 && len(a.Action) == 0 && len(a.LocKey) == 0 && len(a.LocArgs) == 0 && len(a.ActionLocKey) == 0 && len(a.LaunchImage) == 0
}

func (a *Alert) isZero() bool {
	return a.isSimple() && len(a.Body) == 0
}

type APS struct {
	Alert            Alert
	Badge            BadgeNumber
	Sound            string
	ContentAvailable int
	URLArgs          []string
	Category         string // requires iOS 8+
	AccountId        string // for email push notifications
}

func (aps APS) MarshalJSON() ([]byte, error) {
	data := make(map[string]interface{})

	if !aps.Alert.isZero() {
		if aps.Alert.isSimple() {
			data["alert"] = aps.Alert.Body
		} else {
			data["alert"] = aps.Alert
		}
	}
	if aps.Badge.IsSet {
		data["badge"] = aps.Badge
	}
	if aps.Sound != "" {
		data["sound"] = aps.Sound
	}
	if aps.ContentAvailable != 0 {
		data["content-available"] = aps.ContentAvailable
	}
	if aps.Category != "" {
		data["category"] = aps.Category
	}
	if aps.URLArgs != nil && len(aps.URLArgs) != 0 {
		data["url-args"] = aps.URLArgs
	}
	if aps.AccountId != "" {
		data["account-id"] = aps.AccountId
	}

	return json.Marshal(data)
}

type Payload struct {
	APS APS
	// MDM for mobile device management
	MDM          string
	customValues map[string]interface{}
}

func (p *Payload) MarshalJSON() ([]byte, error) {
	if len(p.MDM) != 0 {
		p.customValues["mdm"] = p.MDM
	} else {
		p.customValues["aps"] = p.APS
	}

	return json.Marshal(p.customValues)
}

func (p *Payload) SetCustomValue(key string, value interface{}) error {
	if key == "aps" {
		return errors.New("cannot assign a custom APS value in payload")
	}

	p.customValues[key] = value

	return nil
}

type Notification struct {
	ID          string
	DeviceToken string
	Identifier  uint32
	Expiration  *time.Time
	Priority    int
	Payload     *Payload
}

func NewNotification() Notification {
	return Notification{Payload: NewPayload()}
}

func NewPayload() *Payload {
	return &Payload{customValues: map[string]interface{}{}}
}

func (n Notification) ToBinary() ([]byte, error) {
	b := []byte{}

	binTok, err := hex.DecodeString(n.DeviceToken)
	if err != nil {
		return b, fmt.Errorf("convert token to hex error: %s", err)
	}

	j, _ := json.Marshal(n.Payload)

	buf := bytes.NewBuffer(b)

	// Token
	binary.Write(buf, binary.BigEndian, uint8(deviceTokenItemID))
	binary.Write(buf, binary.BigEndian, uint16(deviceTokenItemLength))
	binary.Write(buf, binary.BigEndian, binTok)

	// Payload
	binary.Write(buf, binary.BigEndian, uint8(payloadItemID))
	binary.Write(buf, binary.BigEndian, uint16(len(j)))
	binary.Write(buf, binary.BigEndian, j)

	// Identifier
	binary.Write(buf, binary.BigEndian, uint8(notificationIdentifierItemID))
	binary.Write(buf, binary.BigEndian, uint16(notificationIdentifierItemLength))
	binary.Write(buf, binary.BigEndian, uint32(n.Identifier))

	// Expiry
	binary.Write(buf, binary.BigEndian, uint8(expirationDateItemID))
	binary.Write(buf, binary.BigEndian, uint16(expirationDateItemLength))
	if n.Expiration == nil {
		binary.Write(buf, binary.BigEndian, uint32(0))
	} else {
		binary.Write(buf, binary.BigEndian, uint32(n.Expiration.Unix()))
	}

	// Priority
	binary.Write(buf, binary.BigEndian, uint8(priorityItemID))
	binary.Write(buf, binary.BigEndian, uint16(priorityItemLength))
	binary.Write(buf, binary.BigEndian, uint8(n.Priority))

	framebuf := bytes.NewBuffer([]byte{})
	binary.Write(framebuf, binary.BigEndian, uint8(commandID))
	binary.Write(framebuf, binary.BigEndian, uint32(buf.Len()))
	binary.Write(framebuf, binary.BigEndian, buf.Bytes())

	return framebuf.Bytes(), nil
}


================================================
FILE: notification_test.go
================================================
package apns_test

import (
	"bytes"
	"encoding/binary"
	"encoding/json"
	"time"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/timehop/apns"
)

var _ = Describe("Notifications", func() {
	Describe("Alert", func() {
		Describe("JSON marshalling", func() {
			Context("only body", func() {
				It("should just have that field", func() {
					a := apns.Alert{Body: "whatup"}

					j, err := json.Marshal(a)

					Expect(err).To(BeNil())
					Expect(j).To(Equal([]byte(`{"body":"whatup"}`)))
				})
			})

			Context("only loc-key", func() {
				It("should just have that field", func() {
					a := apns.Alert{LocKey: "localization"}

					j, err := json.Marshal(a)

					Expect(err).To(BeNil())
					Expect(j).To(Equal([]byte(`{"loc-key":"localization"}`)))
				})
			})

			Context("only loc-args", func() {
				It("should just have that field", func() {
					a := apns.Alert{LocArgs: []string{"world", "cup"}}

					j, err := json.Marshal(a)

					Expect(err).To(BeNil())
					Expect(j).To(Equal([]byte(`{"loc-args":["world","cup"]}`)))
				})
			})

			Context("only action-loc-key", func() {
				It("should just have that field", func() {
					a := apns.Alert{ActionLocKey: "akshun localization"}

					j, err := json.Marshal(a)

					Expect(err).To(BeNil())
					Expect(j).To(Equal([]byte(`{"action-loc-key":"akshun localization"}`)))
				})
			})

			Context("only launch image", func() {
				It("should just have that field", func() {
					a := apns.Alert{LaunchImage: "dee fault"}

					j, err := json.Marshal(a)

					Expect(err).To(BeNil())
					Expect(j).To(Equal([]byte(`{"launch-image":"dee fault"}`)))
				})
			})

			Context("fully loaded", func() {
				It("should serialize", func() {
					a := apns.Alert{Body: "USA scores!", LocKey: "game", LocArgs: []string{"USA", "BRA"}, LaunchImage: "scoreboard"}

					j, err := json.Marshal(a)

					Expect(err).To(BeNil())
					Expect(j).To(Equal([]byte(`{"body":"USA scores!","loc-key":"game","loc-args":["USA","BRA"],"launch-image":"scoreboard"}`)))
				})
			})
		})
	})

	Describe("Safari", func() {
		Describe("#MarshalJSON", func() {
			Context("with complete payload", func() {
				It("should marshal APS", func() {
					p := apns.NewPayload()

					p.APS.Alert.Title = "Hello World!"
					p.APS.Alert.Body = "This is a body"
					p.APS.Alert.Action = "Launch"
					p.APS.URLArgs = []string{"hello", "world"}

					b, err := json.Marshal(p)

					Expect(err).To(BeNil())
					Expect(b).To(Equal([]byte(`{"aps":{"alert":{"body":"This is a body","title":"Hello World!","action":"Launch"},"url-args":["hello","world"]}}`)))
				})
			})
		})
	})

	Describe("Payload", func() {
		Describe("#MarshalJSON", func() {
			Context("no alert (as with Passbook)", func() {
				It("should not contain the alert struct", func() {
					p := apns.NewPayload()
					b, err := json.Marshal(p)

					Expect(err).To(BeNil())
					Expect(b).To(Equal([]byte(`{"aps":{}}`)))
				})
			})
			Context("no alert with content available (as with Newsstand)", func() {
				It("should not contain the alert struct", func() {
					p := apns.NewPayload()
					p.APS.ContentAvailable = 1
					b, err := json.Marshal(p)

					Expect(err).To(BeNil())
					Expect(b).To(Equal([]byte(`{"aps":{"content-available":1}}`)))
				})
			})
			Context("with only APS", func() {
				It("should marshal APS", func() {
					p := apns.NewPayload()

					p.APS.Alert.Body = "testing"

					b, err := json.Marshal(p)

					Expect(err).To(BeNil())
					Expect(b).To(Equal([]byte(`{"aps":{"alert":"testing"}}`)))
				})
			})

			Context("with custom attributes APS", func() {
				It("should marshal APS", func() {
					p := apns.NewPayload()

					p.APS.Alert.Body = "testing"
					p.SetCustomValue("email", "come@me.bro")

					b, err := json.Marshal(p)

					Expect(err).To(BeNil())
					Expect(b).To(Equal([]byte(`{"aps":{"alert":"testing"},"email":"come@me.bro"}`)))
				})
			})

			Context("with only MDM", func() {
				It("should marshal MDM", func() {
					p := apns.NewPayload()

					p.MDM = "00000000-1111-3333-4444-555555555555"

					b, err := json.Marshal(p)

					Expect(err).To(BeNil())
					Expect(b).To(Equal([]byte(`{"mdm":"00000000-1111-3333-4444-555555555555"}`)))
				})
			})
		})
	})

	Describe("APS", func() {
		Context("badge with a zero (clears notifications)", func() {
			It("should contain zero", func() {
				a := apns.APS{}
				a.Badge.Set(0)

				j, err := json.Marshal(a)

				Expect(err).To(BeNil())
				Expect(j).To(Equal([]byte(`{"badge":0}`)))
			})
		})
		Context("no badge specified (do nothing)", func() {
			It("should omit the badge field", func() {
				a := apns.APS{}

				j, err := json.Marshal(a)

				Expect(err).To(BeNil())
				Expect(j).To(Equal([]byte(`{}`)))
			})
		})
		Context("email account id", func() {
			It("should contain the account id", func() {
				a := apns.APS{AccountId: "1234"}

				j, err := json.Marshal(a)

				Expect(err).To(BeNil())
				Expect(j).To(Equal([]byte(`{"account-id":"1234"}`)))
			})
		})
	})

	Describe("Notification", func() {
		Describe("#ToBinary", func() {
			Context("invalid token format", func() {
				n := apns.NewNotification()
				n.DeviceToken = "totally not a valid token"

				It("should return an error", func() {
					_, err := n.ToBinary()
					Expect(err).NotTo(BeNil())
					Expect(err.Error()).To(ContainSubstring("convert token to hex error"))
				})
			})

			Context("valid payload", func() {
				It("should generate the correct byte payload with expiry", func() {
					t := time.Unix(1404102833, 0)

					n := apns.NewNotification()
					n.Identifier = uint32(123123)
					n.DeviceToken = "9999999999999999999999999999999999999999999999999999999999999999"
					n.Priority = apns.PriorityImmediate
					n.Expiration = &t

					b, err := n.ToBinary()

					Expect(err).To(BeNil())

					buf := bytes.NewBuffer(b)

					var expiry uint32

					buf.Next(1 + 4 + 1 + 2 + 32 + 1 + 2 + 20 + 1 + 2 + 4 + 1 + 2)

					// Expiry
					binary.Read(buf, binary.BigEndian, &expiry)
				})

				It("should generate the correct byte payload", func() {
					n := apns.NewNotification()
					n.Identifier = uint32(123123)
					n.DeviceToken = "9999999999999999999999999999999999999999999999999999999999999999"
					n.Priority = apns.PriorityImmediate
					b, err := n.ToBinary()

					Expect(err).To(BeNil())

					buf := bytes.NewBuffer(b)

					var command, tokID, payloadID, identifierID, expiryID, priorityID uint8
					var tokLen, payloadLen, identifierLen, expiryLen, priorityLen uint16
					var frameLen, identifier, expiry uint32
					var priority byte
					var tok [32]byte
					var payload [10]byte

					binary.Read(buf, binary.BigEndian, &command)
					binary.Read(buf, binary.BigEndian, &frameLen)

					// Token
					binary.Read(buf, binary.BigEndian, &tokID)
					binary.Read(buf, binary.BigEndian, &tokLen)
					binary.Read(buf, binary.BigEndian, &tok)

					// Payload
					binary.Read(buf, binary.BigEndian, &payloadID)
					binary.Read(buf, binary.BigEndian, &payloadLen)
					binary.Read(buf, binary.BigEndian, &payload)

					// Identifier
					binary.Read(buf, binary.BigEndian, &identifierID)
					binary.Read(buf, binary.BigEndian, &identifierLen)
					binary.Read(buf, binary.BigEndian, &identifier)

					// Expiry
					binary.Read(buf, binary.BigEndian, &expiryID)
					binary.Read(buf, binary.BigEndian, &expiryLen)
					binary.Read(buf, binary.BigEndian, &expiry)

					// Priority
					binary.Read(buf, binary.BigEndian, &priorityID)
					binary.Read(buf, binary.BigEndian, &priorityLen)
					binary.Read(buf, binary.BigEndian, &priority)

					Expect(command).To(Equal(uint8(2)))
					Expect(frameLen).To(Equal(uint32(66)))

					// Token
					Expect(tokID).To(Equal(uint8(1)))
					Expect(tokLen).To(Equal(uint16(32)))
					Expect(tok).To(Equal([32]byte{153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153}))

					// Payload
					Expect(payloadID).To(Equal(uint8(2)))
					Expect(payloadLen).To(Equal(uint16(10)))
					Expect(payload).To(Equal([10]byte{123, 34, 97, 112, 115, 34, 58, 123, 125, 125}))

					// Identifier
					Expect(identifierID).To(Equal(uint8(3)))
					Expect(identifierLen).To(Equal(uint16(4)))
					Expect(identifier).To(Equal(uint32(123123)))

					// Expiry
					Expect(expiryID).To(Equal(uint8(4)))
					Expect(expiryLen).To(Equal(uint16(4)))
					Expect(expiry).To(Equal(uint32(0)))

					// Priority
					Expect(priorityID).To(Equal(uint8(5)))
					Expect(priorityLen).To(Equal(uint16(1)))
					Expect(priority).To(Equal(uint8(10)))
				})
			})
		})
	})
})
Download .txt
gitextract_u9iwx99q/

├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── apns_suite_test.go
├── badge_number.go
├── badge_number_test.go
├── client.go
├── client_test.go
├── conn.go
├── conn_test.go
├── doc.go
├── error.go
├── error_test.go
├── example/
│   └── example.go
├── feedback.go
├── feedback_test.go
├── go.mod
├── go.sum
├── notification.go
└── notification_test.go
Download .txt
SYMBOL INDEX (100 symbols across 9 files)

FILE: apns_suite_test.go
  function TestApns (line 10) | func TestApns(t *testing.T) {

FILE: badge_number.go
  type BadgeNumber (line 10) | type BadgeNumber struct
    method Unset (line 17) | func (b *BadgeNumber) Unset() {
    method Set (line 25) | func (b *BadgeNumber) Set(number uint) {
    method MarshalJSON (line 32) | func (b BadgeNumber) MarshalJSON() ([]byte, error) {
    method UnmarshalJSON (line 40) | func (b *BadgeNumber) UnmarshalJSON(data []byte) error {

FILE: client.go
  type buffer (line 11) | type buffer struct
    method Add (line 20) | func (b *buffer) Add(v interface{}) *list.Element {
  function newBuffer (line 16) | func newBuffer(size int) *buffer {
  type Client (line 30) | type Client struct
    method Send (line 75) | func (c *Client) Send(n Notification) error {
    method reportFailedPush (line 80) | func (c *Client) reportFailedPush(v interface{}, err *Error) {
    method requeue (line 92) | func (c *Client) requeue(cursor *list.Element) {
    method handleError (line 102) | func (c *Client) handleError(err *Error, buffer *buffer) *list.Element {
    method runLoop (line 125) | func (c *Client) runLoop() {
  function newClientWithConn (line 38) | func newClientWithConn(gw string, conn Conn) Client {
  function NewClientWithCert (line 51) | func NewClientWithCert(gw string, cert tls.Certificate) Client {
  function NewClient (line 57) | func NewClient(gw string, cert string, key string) (Client, error) {
  function NewClientWithFiles (line 66) | func NewClientWithFiles(gw string, certFile string, keyFile string) (Cli...
  function readErrs (line 203) | func readErrs(c *Conn) chan error {

FILE: conn.go
  constant ProductionGateway (line 10) | ProductionGateway = "gateway.push.apple.com:2195"
  constant SandboxGateway (line 11) | SandboxGateway    = "gateway.sandbox.push.apple.com:2195"
  constant ProductionFeedbackGateway (line 13) | ProductionFeedbackGateway = "feedback.push.apple.com:2196"
  constant SandboxFeedbackGateway (line 14) | SandboxFeedbackGateway    = "feedback.sandbox.push.apple.com:2196"
  type Conn (line 18) | type Conn struct
    method Connect (line 57) | func (c *Conn) Connect() error {
    method Close (line 78) | func (c *Conn) Close() error {
    method Read (line 87) | func (c *Conn) Read(p []byte) (int, error) {
    method Write (line 93) | func (c *Conn) Write(p []byte) (int, error) {
  function NewConnWithCert (line 26) | func NewConnWithCert(gw string, cert tls.Certificate) Conn {
  function NewConn (line 37) | func NewConn(gw string, crt string, key string) (Conn, error) {
  function NewConnWithFiles (line 47) | func NewConnWithFiles(gw string, certFile string, keyFile string) (Conn,...

FILE: conn_test.go
  type mockAddr (line 70) | type mockAddr struct
    method Network (line 73) | func (m mockAddr) Network() string {
    method String (line 77) | func (m mockAddr) String() string {
  type mockTLSNetConn (line 82) | type mockTLSNetConn struct
    method Read (line 87) | func (t mockTLSNetConn) Read(p []byte) (int, error) {
    method Write (line 92) | func (t mockTLSNetConn) Write(p []byte) (int, error) {
    method Close (line 96) | func (t mockTLSNetConn) Close() error {
    method LocalAddr (line 100) | func (m mockTLSNetConn) LocalAddr() net.Addr {
    method RemoteAddr (line 104) | func (m mockTLSNetConn) RemoteAddr() net.Addr {
    method SetDeadline (line 108) | func (m mockTLSNetConn) SetDeadline(t time.Time) error {
    method SetReadDeadline (line 112) | func (m mockTLSNetConn) SetReadDeadline(t time.Time) error {
    method SetWriteDeadline (line 116) | func (m mockTLSNetConn) SetWriteDeadline(t time.Time) error {
  type serverAction (line 120) | type serverAction struct
  constant readAction (line 127) | readAction  = "read"
  constant writeAction (line 128) | writeAction = "write"
  constant closeAction (line 129) | closeAction = "close"
  type mockTLSServer (line 132) | type mockTLSServer struct
    method portStr (line 138) | func (m *mockTLSServer) portStr() string {
    method Address (line 147) | func (m *mockTLSServer) Address() string {
    method start (line 151) | func (m *mockTLSServer) start() {
    method stop (line 204) | func (m *mockTLSServer) stop() {

FILE: error.go
  constant ErrProcessing (line 11) | ErrProcessing         = "Processing error"
  constant ErrMissingDeviceToken (line 12) | ErrMissingDeviceToken = "Missing device token"
  constant ErrMissingTopic (line 13) | ErrMissingTopic       = "Missing topic"
  constant ErrMissingPayload (line 14) | ErrMissingPayload     = "Missing payload"
  constant ErrInvalidTokenSize (line 15) | ErrInvalidTokenSize   = "Invalid token size"
  constant ErrInvalidTopicSize (line 16) | ErrInvalidTopicSize   = "Invalid topic size"
  constant ErrInvalidPayloadSize (line 17) | ErrInvalidPayloadSize = "Invalid payload size"
  constant ErrInvalidToken (line 18) | ErrInvalidToken       = "Invalid token"
  constant ErrShutdown (line 19) | ErrShutdown           = "Shutdown"
  constant ErrUnknown (line 20) | ErrUnknown            = "None (unknown)"
  type Error (line 36) | type Error struct
    method Error (line 63) | func (e *Error) Error() string {
  function NewError (line 43) | func NewError(p []byte) Error {

FILE: example/example.go
  function main (line 10) | func main() {

FILE: feedback.go
  type Feedback (line 11) | type Feedback struct
    method Receive (line 66) | func (f Feedback) Receive() <-chan FeedbackTuple {
    method receive (line 72) | func (f Feedback) receive(fc chan FeedbackTuple) {
  type FeedbackTuple (line 15) | type FeedbackTuple struct
  function feedbackTupleFromBytes (line 21) | func feedbackTupleFromBytes(b []byte) FeedbackTuple {
  function NewFeedbackWithCert (line 40) | func NewFeedbackWithCert(gw string, cert tls.Certificate) Feedback {
  function NewFeedback (line 46) | func NewFeedback(gw string, cert string, key string) (Feedback, error) {
  function NewFeedbackWithFiles (line 55) | func NewFeedbackWithFiles(gw string, certFile string, keyFile string) (F...

FILE: notification.go
  constant PriorityImmediate (line 14) | PriorityImmediate     = 10
  constant PriorityPowerConserve (line 15) | PriorityPowerConserve = 5
  constant commandID (line 19) | commandID = 2
  constant deviceTokenItemID (line 22) | deviceTokenItemID            = 1
  constant payloadItemID (line 23) | payloadItemID                = 2
  constant notificationIdentifierItemID (line 24) | notificationIdentifierItemID = 3
  constant expirationDateItemID (line 25) | expirationDateItemID         = 4
  constant priorityItemID (line 26) | priorityItemID               = 5
  constant deviceTokenItemLength (line 29) | deviceTokenItemLength            = 32
  constant notificationIdentifierItemLength (line 30) | notificationIdentifierItemLength = 4
  constant expirationDateItemLength (line 31) | expirationDateItemLength         = 4
  constant priorityItemLength (line 32) | priorityItemLength               = 1
  type NotificationResult (line 35) | type NotificationResult struct
  type Alert (line 40) | type Alert struct
    method isSimple (line 51) | func (a *Alert) isSimple() bool {
    method isZero (line 55) | func (a *Alert) isZero() bool {
  type APS (line 59) | type APS struct
    method MarshalJSON (line 69) | func (aps APS) MarshalJSON() ([]byte, error) {
  type Payload (line 101) | type Payload struct
    method MarshalJSON (line 108) | func (p *Payload) MarshalJSON() ([]byte, error) {
    method SetCustomValue (line 118) | func (p *Payload) SetCustomValue(key string, value interface{}) error {
  type Notification (line 128) | type Notification struct
    method ToBinary (line 145) | func (n Notification) ToBinary() ([]byte, error) {
  function NewNotification (line 137) | func NewNotification() Notification {
  function NewPayload (line 141) | func NewPayload() *Payload {
Condensed preview — 21 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (80K chars).
[
  {
    "path": ".gitignore",
    "chars": 348,
    "preview": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture spe"
  },
  {
    "path": ".travis.yml",
    "chars": 336,
    "preview": "language: go\ngo:\n  - 1.3\nservices:\n  - redis-server\nbefore_script:\n  - go get github.com/onsi/ginkgo\n  - go get github.c"
  },
  {
    "path": "LICENSE",
    "chars": 1073,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2014 timehop\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "README.md",
    "chars": 4016,
    "preview": "# apns\n\n[![GoDoc](https://godoc.org/github.com/timehop/apns?status.svg)](https://godoc.org/github.com/timehop/apns)\n[![B"
  },
  {
    "path": "apns_suite_test.go",
    "chars": 185,
    "preview": "package apns_test\n\nimport (\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n\n\t\"testing\"\n)\n\nfunc TestApns(t *test"
  },
  {
    "path": "badge_number.go",
    "chars": 1326,
    "preview": "package apns\n\nimport \"encoding/json\"\n\n// BadgeNumber is much a NullInt64 in\n// database/sql except instead of using\n// t"
  },
  {
    "path": "badge_number_test.go",
    "chars": 1939,
    "preview": "package apns_test\n\nimport (\n\t\"encoding/json\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n\t\"github.com/timeh"
  },
  {
    "path": "client.go",
    "chars": 4206,
    "preview": "package apns\n\nimport (\n\t\"container/list\"\n\t\"crypto/tls\"\n\t\"io\"\n\t\"log\"\n\t\"time\"\n)\n\ntype buffer struct {\n\tsize int\n\t*list.Lis"
  },
  {
    "path": "client_test.go",
    "chars": 9920,
    "preview": "package apns_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"time\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"githu"
  },
  {
    "path": "conn.go",
    "chars": 2040,
    "preview": "package apns\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"strings\"\n)\n\nconst (\n\tProductionGateway = \"gateway.push.apple.com:2195\"\n\tSa"
  },
  {
    "path": "conn_test.go",
    "chars": 10659,
    "preview": "package apns_test\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\t. \""
  },
  {
    "path": "doc.go",
    "chars": 1323,
    "preview": "/*\nA Go package to interface with the Apple Push\nNotification Service\n\nFeatures\n\nThis library implements a few features "
  },
  {
    "path": "error.go",
    "chars": 1547,
    "preview": "package apns\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n)\n\nconst (\n\t// Error strings based on the codes specified here:\n\t// h"
  },
  {
    "path": "error_test.go",
    "chars": 2588,
    "preview": "package apns_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"math/rand\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/go"
  },
  {
    "path": "example/example.go",
    "chars": 783,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/timehop/apns\"\n)\n\nfunc main() {\n\tc, err := apns.NewClientWithFiles(apn"
  },
  {
    "path": "feedback.go",
    "chars": 1789,
    "preview": "package apns\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"time\"\n)\n\ntype Feedback struct {\n\tConn"
  },
  {
    "path": "feedback_test.go",
    "chars": 4438,
    "preview": "package apns_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"time\"\n\t. \"github.com/onsi/g"
  },
  {
    "path": "go.mod",
    "chars": 480,
    "preview": "module github.com/timehop/apns\n\ngo 1.20\n\nrequire (\n\tgithub.com/onsi/ginkgo v1.16.5\n\tgithub.com/onsi/gomega v1.27.6\n)\n\nre"
  },
  {
    "path": "go.sum",
    "chars": 8828,
    "preview": "github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1"
  },
  {
    "path": "notification.go",
    "chars": 4687,
    "preview": "package apns\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n)\n\nconst (\n"
  },
  {
    "path": "notification_test.go",
    "chars": 8651,
    "preview": "package apns_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github"
  }
]

About this extraction

This page contains the full source code of the timehop/apns GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 21 files (69.5 KB), approximately 24.0k tokens, and a symbol index with 100 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!