diff --git a/auth/interface.go b/auth/interface.go
index ea1b108744ed2fceb201eb6268c2d54d684f8002..b45ba36e9c7d4a135a0f7adf48416b1e1cbf1112 100644
--- a/auth/interface.go
+++ b/auth/interface.go
@@ -150,5 +150,6 @@ type e2eHandler interface {
 		receiveParams session.Params) (partner.Manager, error)
 	GetPartner(partnerID *id.ID) (partner.Manager, error)
 	DeletePartner(partnerId *id.ID) error
+	DeletePartnerNotify(partnerId *id.ID, params e2e.Params) error
 	GetReceptionID() *id.ID
 }
diff --git a/auth/state_test.go b/auth/state_test.go
index 10ebb2d3ed4d9d99c654da78dfacc260a877a771..34b670453b4a5052d71868ebeccd4d8030f1a511 100644
--- a/auth/state_test.go
+++ b/auth/state_test.go
@@ -8,6 +8,7 @@
 package auth
 
 import (
+	"gitlab.com/elixxir/client/e2e"
 	"io"
 	"math/rand"
 	"testing"
@@ -100,6 +101,9 @@ func (me2e *mockE2E) GetPartner(partnerID *id.ID) (partner.Manager, error) {
 func (me2e *mockE2E) DeletePartner(partnerId *id.ID) error {
 	return nil
 }
+func (me2e *mockE2E) DeletePartnerNotify(partnerId *id.ID, params e2e.Params) error {
+	return nil
+}
 func (me2e *mockE2E) GetReceptionID() *id.ID {
 	return me2e.reception
 }
diff --git a/auth/utils_test.go b/auth/utils_test.go
index 4c53ff91a95c40e4b138258592cb292b12d8e749..5f27a218e0651736ace0ca165ef44e0ef581b05d 100644
--- a/auth/utils_test.go
+++ b/auth/utils_test.go
@@ -105,6 +105,10 @@ func (m mockE2eHandler) DeletePartner(partnerId *id.ID) error {
 	return nil
 }
 
+func (m mockE2eHandler) DeletePartnerNotify(partnerId *id.ID, params e2e.Params) error {
+	return nil
+}
+
 func (m mockE2eHandler) GetAllPartnerIDs() []*id.ID {
 	return nil
 }
@@ -139,6 +143,18 @@ func (m mockE2eHandler) GetReceptionID() *id.ID {
 	return nil
 }
 
+func (m mockE2eHandler) RegisterCallbacks(callbacks e2e.Callbacks) {
+	panic("implement me")
+}
+
+func (m mockE2eHandler) AddPartnerCallbacks(partnerID *id.ID, cb e2e.Callbacks) {
+	panic("implement me")
+}
+
+func (m mockE2eHandler) DeletePartnerCallbacks(partnerID *id.ID) {
+	panic("implement me")
+}
+
 type mockSentRequestHandler struct{}
 
 func (msrh *mockSentRequestHandler) Add(sr *store.SentRequest)    {}
diff --git a/catalog/messageTypes.go b/catalog/messageTypes.go
index 6fc09ead3c9520f5db635ebf29da7e160698cc00..9ab1244631e1c6265e2f8a4a205022f35d943ac0 100644
--- a/catalog/messageTypes.go
+++ b/catalog/messageTypes.go
@@ -32,6 +32,10 @@ const (
 	// to confirm completion of a rekey. For ephemeral only e2e instances.
 	KeyExchangeConfirmEphemeral = 33
 
+	// E2eClose message is sent when a user deletes a partner and wants to
+	// inform their partner that the connection is closed.
+	E2eClose MessageType = 34
+
 	/* Group chat message types */
 
 	// GroupCreationRequest - A group chat request message sent to all members in a group.
@@ -65,6 +69,8 @@ func (mt MessageType) String() string {
 		return "KeyExchangeTriggerEphemeral"
 	case KeyExchangeConfirmEphemeral:
 		return "KeyExchangeConfirmEphemeral"
+	case E2eClose:
+		return "E2eClose"
 	case GroupCreationRequest:
 		return "GroupCreationRequest"
 	case NewFileTransfer:
diff --git a/e2e/callbacks.go b/e2e/callbacks.go
new file mode 100644
index 0000000000000000000000000000000000000000..915229bd040440511c21caa0666dea6a68f5950d
--- /dev/null
+++ b/e2e/callbacks.go
@@ -0,0 +1,61 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package e2e
+
+import (
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/xx_network/primitives/id"
+	"sync"
+)
+
+// partnerCallbacks is a thread-safe wrapper for Callbacks specific to partner
+// IDs. For E2E operations with a specific partner, these Callbacks will be used
+// instead.
+type partnerCallbacks struct {
+	callbacks map[id.ID]Callbacks
+	sync.RWMutex
+}
+
+// newPartnerCallbacks initializes an empty partnerCallbacks.
+func newPartnerCallbacks() *partnerCallbacks {
+	return &partnerCallbacks{
+		callbacks: make(map[id.ID]Callbacks),
+	}
+}
+
+// add registers Callbacks that override the generic E2E callback for the given
+// partner ID.
+func (pcb *partnerCallbacks) add(partnerID *id.ID, cbs Callbacks) {
+	pcb.Lock()
+	defer pcb.Unlock()
+	pcb.callbacks[*partnerID] = cbs
+}
+
+// delete deletes the callbacks for the given partner ID.
+func (pcb *partnerCallbacks) delete(partnerID *id.ID) {
+	pcb.Lock()
+	defer pcb.Unlock()
+	delete(pcb.callbacks, *partnerID)
+}
+
+// get returns the Callbacks for the given partner ID.
+func (pcb *partnerCallbacks) get(partnerID *id.ID) Callbacks {
+	pcb.RLock()
+	defer pcb.RUnlock()
+
+	return pcb.callbacks[*partnerID]
+}
+
+// DefaultCallbacks is a simple structure for providing a default Callbacks
+// implementation. It should generally not be used.
+type DefaultCallbacks struct{}
+
+func (d *DefaultCallbacks) ConnectionClosed(*id.ID, rounds.Round) {
+	jww.ERROR.Printf("No valid e2e callback assigned!")
+}
diff --git a/e2e/callbacks_test.go b/e2e/callbacks_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..327020f04304e6b960f3bc72e7c6916f9c61ad7c
--- /dev/null
+++ b/e2e/callbacks_test.go
@@ -0,0 +1,110 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package e2e
+
+import (
+	"gitlab.com/elixxir/client/cmix/rounds"
+	"gitlab.com/xx_network/primitives/id"
+	"reflect"
+	"testing"
+)
+
+// Tests that newPartnerCallbacks returns the expected new partnerCallbacks.
+func Test_newPartnerCallbacks(t *testing.T) {
+	expected := &partnerCallbacks{
+		callbacks: make(map[id.ID]Callbacks),
+	}
+
+	pcb := newPartnerCallbacks()
+
+	if !reflect.DeepEqual(expected, pcb) {
+		t.Errorf("Did not get expected new partnerCallbacks."+
+			"\nexpected: %+v\nreceived: %+v", expected, pcb)
+	}
+}
+
+// Tests that partnerCallbacks.add adds all the expected callbacks to the map.
+func Test_partnerCallbacks_add(t *testing.T) {
+	pcb := newPartnerCallbacks()
+
+	const n = 10
+	expected := make(map[id.ID]Callbacks, n)
+	for i := uint64(0); i < n; i++ {
+		expected[*id.NewIdFromUInt(i, id.User, t)] = &mockCallbacks{id: i}
+	}
+
+	for partnerID, cbs := range expected {
+		pcb.add(&partnerID, cbs)
+	}
+
+	if !reflect.DeepEqual(expected, pcb.callbacks) {
+		t.Errorf("Callback list does not match expected."+
+			"\nexpected: %v\nreceived: %v", expected, pcb.callbacks)
+	}
+}
+
+// Tests that partnerCallbacks.delete removes all callbacks from the map.
+func Test_partnerCallbacks_delete(t *testing.T) {
+	pcb := newPartnerCallbacks()
+
+	const n = 10
+	expected := make(map[id.ID]Callbacks, n)
+	for i := uint64(0); i < n; i++ {
+		partnerID, cbs := id.NewIdFromUInt(i, id.User, t), &mockCallbacks{id: i}
+		expected[*partnerID] = cbs
+		pcb.add(partnerID, cbs)
+	}
+
+	for partnerID := range expected {
+		pcb.delete(&partnerID)
+	}
+
+	if len(pcb.callbacks) > 0 {
+		t.Errorf("Callback map not empty: %v", pcb.callbacks)
+	}
+}
+
+// Tests that partnerCallbacks.get returns the expected Callbacks for each
+// partner ID.
+func Test_partnerCallbacks_get(t *testing.T) {
+	pcb := newPartnerCallbacks()
+
+	const n = 10
+	expected := make(map[id.ID]Callbacks, n)
+	for i := uint64(0); i < n; i++ {
+		partnerID, cbs := id.NewIdFromUInt(i, id.User, t), &mockCallbacks{id: i}
+		expected[*partnerID] = cbs
+		pcb.add(partnerID, cbs)
+	}
+
+	for partnerID, expectedCbs := range expected {
+		cbs := pcb.get(&partnerID)
+		if !reflect.DeepEqual(expectedCbs, cbs) {
+			t.Errorf("Callbacks for parter %s do not match."+
+				"\nexpected: %+v\nreceived: %+v", &partnerID, expectedCbs, cbs)
+		}
+	}
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Mock Callbacks                                                             //
+////////////////////////////////////////////////////////////////////////////////
+
+// Verify that mockCallbacks adhere to the Callbacks interface
+var _ Callbacks = (*mockCallbacks)(nil)
+
+// mockCallbacks is a structure used for testing that adheres to the Callbacks
+// interface.
+type mockCallbacks struct {
+	id                   uint64
+	connectionClosedChan chan *id.ID
+}
+
+func (m *mockCallbacks) ConnectionClosed(partner *id.ID, _ rounds.Round) {
+	m.connectionClosedChan <- partner
+}
diff --git a/e2e/interface.go b/e2e/interface.go
index c4c6b68af6ccacfc85876ce964120a5868e3e4a2..0578bbc719731f5d1c83a7e16330506aac8bac66 100644
--- a/e2e/interface.go
+++ b/e2e/interface.go
@@ -1,6 +1,7 @@
 package e2e
 
 import (
+	"gitlab.com/elixxir/client/cmix/rounds"
 	"time"
 
 	"github.com/cloudflare/circl/dh/sidh"
@@ -121,10 +122,15 @@ type Handler interface {
 	// GetPartner returns the partner per its ID, if it exists
 	GetPartner(partnerID *id.ID) (partner.Manager, error)
 
-	// DeletePartner removes the associated contact from the E2E store
-	// myID is your ID in the relationship
+	// DeletePartner removes the contact associated with the partnerId from the
+	// E2E store.
 	DeletePartner(partnerId *id.ID) error
 
+	// DeletePartnerNotify removes the contact associated with the partnerId
+	// from the E2E store. It then sends a critical E2E message to the partner
+	// informing them that the E2E connection is closed.
+	DeletePartnerNotify(partnerId *id.ID, params Params) error
+
 	// GetAllPartnerIDs returns a list of all partner IDs that the user has
 	// an E2E relationship with.
 	GetAllPartnerIDs() []*id.ID
@@ -149,6 +155,31 @@ type Handler interface {
 	// RemoveService removes all services for the given tag
 	RemoveService(tag string) error
 
+	/* === Callbacks ==================================================== */
+
+	// The E2E callbacks are a set of callbacks that are called in specific
+	// situations. For example, ConnectionClosed is called when you receive a
+	// message from a partner informing you they have deleted the partnership.
+	//
+	// By default, on E2E creation, callbacks are not set and no action is
+	// taken. To set generic callbacks, that is used for all partners, use
+	// RegisterCallbacks. Specific callbacks can be registered per user that are
+	// used instead of the generic ones.
+
+	// RegisterCallbacks registers a generic Callbacks. This function overwrites
+	// any previously saved Callbacks. By default, these callbacks are nil and
+	// ignored until set via this function.
+	RegisterCallbacks(callbacks Callbacks)
+
+	// AddPartnerCallbacks registers a new Callbacks that overrides the generic
+	// E2E callbacks for the given partner ID.
+	AddPartnerCallbacks(partnerID *id.ID, cb Callbacks)
+
+	// DeletePartnerCallbacks deletes the Callbacks that override the generic
+	// E2E callback for the given partner ID. Deleting these callbacks will
+	// result in the generic E2E callbacks being used.
+	DeletePartnerCallbacks(partnerID *id.ID)
+
 	/* === Unsafe ======================================================= */
 
 	// SendUnsafe sends a message without encryption. It breaks
@@ -198,3 +229,12 @@ type Handler interface {
 	// message
 	PayloadSize() uint
 }
+
+// Callbacks contains the possible callbacks on E2E.
+type Callbacks interface {
+	// ConnectionClosed is called when you receive a message from a partner
+	// informing you that they have deleted the partnership and will no longer
+	// receive messages. It is called when a catalog.E2eClose E2E message is
+	// received.
+	ConnectionClosed(partner *id.ID, round rounds.Round)
+}
diff --git a/e2e/manager.go b/e2e/manager.go
index 00630b7467ad57d1eb21a653b6a3315061f79c24..3bebd7dae8595cb20285dac8abcb509f7b811965 100644
--- a/e2e/manager.go
+++ b/e2e/manager.go
@@ -1,7 +1,10 @@
 package e2e
 
 import (
+	"bytes"
+	"encoding/base64"
 	"encoding/json"
+	"sync"
 	"time"
 
 	jww "github.com/spf13/jwalterweatherman"
@@ -37,6 +40,14 @@ type manager struct {
 	crit        *critical
 	rekeyParams rekey.Params
 	kv          *versioned.KV
+
+	// Generic Callbacks for all E2E operations; by default this is nil and
+	// ignored until set via RegisterCallbacks
+	callbacks Callbacks
+	cbMux     sync.Mutex
+
+	// Partner-specific Callbacks
+	partnerCallbacks *partnerCallbacks
 }
 
 const legacyE2EKey = "legacyE2ESystem"
@@ -139,13 +150,15 @@ func loadE2E(kv *versioned.KV, net cmix.Client, myDefaultID *id.ID,
 	events event.Reporter) (Handler, error) {
 
 	m := &manager{
-		Switchboard: receive.New(),
-		net:         net,
-		myID:        myDefaultID,
-		events:      events,
-		grp:         grp,
-		rekeyParams: rekey.Params{},
-		kv:          kv,
+		Switchboard:      receive.New(),
+		net:              net,
+		myID:             myDefaultID,
+		events:           events,
+		grp:              grp,
+		rekeyParams:      rekey.Params{},
+		kv:               kv,
+		callbacks:        nil,
+		partnerCallbacks: newPartnerCallbacks(),
 	}
 	var err error
 
@@ -166,9 +179,22 @@ func loadE2E(kv *versioned.KV, net cmix.Client, myDefaultID *id.ID,
 			"Failed to unmarshal rekeyParams data")
 	}
 
+	// Register listener that calls the ConnectionClosed callback when a
+	// catalog.E2eClose message is received
+	m.Switchboard.RegisterFunc(
+		"connectionClosing", &id.ZeroUser, catalog.E2eClose, m.closeE2eListener)
+
 	return m, nil
 }
 
+// RegisterCallbacks registers the Callbacks to E2E. This function overwrite any
+// previously saved Callbacks.
+func (m *manager) RegisterCallbacks(callbacks Callbacks) {
+	m.cbMux.Lock()
+	defer m.cbMux.Unlock()
+	m.callbacks = callbacks
+}
+
 func (m *manager) StartProcesses() (stoppable.Stoppable, error) {
 	multi := stoppable.NewMulti("e2eManager")
 
@@ -206,6 +232,120 @@ func (m *manager) StartProcesses() (stoppable.Stoppable, error) {
 	return multi, nil
 }
 
+// DeletePartner removes the contact associated with the partnerId from the E2E
+// store.
+func (m *manager) DeletePartner(partnerId *id.ID) error {
+	err := m.Ratchet.DeletePartner(partnerId)
+	if err != nil {
+		return err
+	}
+
+	m.DeletePartnerCallbacks(partnerId)
+
+	return m.Ratchet.DeletePartner(partnerId)
+}
+
+// DeletePartnerNotify removes the contact associated with the partnerId
+// from the E2E store. It then sends a critical E2E message to the partner
+// informing them that the E2E connection is closed.
+func (m *manager) DeletePartnerNotify(partnerId *id.ID, params Params) error {
+	// Check if the partner exists
+	p, err := m.GetPartner(partnerId)
+	if err != nil {
+		return err
+	}
+
+	// Enable critical message sending
+	params.Critical = true
+
+	// Setting the connection fingerprint as the payload allows the receiver
+	// to verify that this catalog.E2eClose message is from this specific
+	// E2E relationship. However, this is not strictly necessary since this
+	// message should not be received by any future E2E connection with the
+	// same partner. This is done as a sanity check and to plainly show
+	// which relationship this message belongs to.
+	payload := p.ConnectionFingerprint().Bytes()
+
+	// Prepare an E2E message informing the partner that you are closing the E2E
+	// connection. The send is prepared before deleting the partner because the
+	// partner needs to be available to build the E2E message. The message is
+	// not sent first to avoid sending the partner an erroneous message of
+	// deletion fails.
+	sendFunc, err := m.prepareSendE2E(
+		catalog.E2eClose, partnerId, payload, params)
+	if err != nil {
+		return err
+	}
+
+	err = m.Ratchet.DeletePartner(partnerId)
+	if err != nil {
+		return err
+	}
+
+	m.DeletePartnerCallbacks(partnerId)
+
+	// Send closing E2E message
+	rounds, msgID, timestamp, err := sendFunc()
+	if err != nil {
+		jww.ERROR.Printf("Failed to send %s E2E message to %s: %+v",
+			catalog.E2eClose, partnerId, err)
+	} else {
+		jww.INFO.Printf(
+			"Sent %s E2E message to %s on rounds %v with message ID %s at %s",
+			catalog.E2eClose, partnerId, rounds, msgID, timestamp)
+	}
+
+	return nil
+}
+
+// closeE2eListener calls the ConnectionClose callback when a catalog.E2eClose
+// message is received from a partner.
+func (m *manager) closeE2eListener(item receive.Message) {
+	p, err := m.GetPartner(item.Sender)
+	if err != nil {
+		jww.ERROR.Printf("Could not find sender %s of %s message: %+v",
+			item.Sender, catalog.E2eClose, err)
+		return
+	}
+
+	// Check the connection fingerprint to verify that the message is
+	// from the expected E2E relationship (refer to the comment in
+	// DeletePartner for more details)
+	if !bytes.Equal(p.ConnectionFingerprint().Bytes(), item.Payload) {
+		jww.ERROR.Printf("Received %s message from %s with incorrect "+
+			"connection fingerprint %s.", catalog.E2eClose, item.Sender,
+			base64.StdEncoding.EncodeToString(item.Payload))
+		return
+	}
+
+	jww.INFO.Printf("Received %s message from %s for relationship %s. "+
+		"Calling ConnectionClosed callback.",
+		catalog.E2eClose, item.Sender, p.ConnectionFingerprint())
+
+	if cb := m.partnerCallbacks.get(item.Sender); cb != nil {
+		cb.ConnectionClosed(item.Sender, item.Round)
+	} else if m.callbacks != nil {
+		m.cbMux.Lock()
+		m.callbacks.ConnectionClosed(item.Sender, item.Round)
+		m.cbMux.Unlock()
+	} else {
+		jww.INFO.Printf("No ConnectionClosed callback found.")
+	}
+}
+
+// AddPartnerCallbacks registers a new Callbacks that overrides the generic
+// e2e callbacks for the given partner ID.
+func (m *manager) AddPartnerCallbacks(partnerID *id.ID, cb Callbacks) {
+	m.partnerCallbacks.add(partnerID, cb)
+}
+
+// DeletePartnerCallbacks deletes the Callbacks that override the generic
+// e2e callback for the given partner ID. Deleting these callbacks will
+// result in the generic e2e callbacks being used.
+func (m *manager) DeletePartnerCallbacks(partnerID *id.ID) {
+	m.partnerCallbacks.delete(partnerID)
+}
+
 // EnableUnsafeReception enables the reception of unsafe message by registering
 // bespoke services for reception. For debugging only!
 func (m *manager) EnableUnsafeReception() {
diff --git a/e2e/sendE2E.go b/e2e/sendE2E.go
index 159f7c8451ca997d3924803f01bb30eb4355d0f1..284008a92d92988b56abb6f6e81fd49381be94e0 100644
--- a/e2e/sendE2E.go
+++ b/e2e/sendE2E.go
@@ -44,15 +44,26 @@ func (m *manager) SendE2E(mt catalog.MessageType, recipient *id.ID,
 
 }
 
-func (m *manager) sendE2E(mt catalog.MessageType, recipient *id.ID,
-	payload []byte, params Params) ([]id.Round, e2e.MessageID, time.Time, error) {
+// sendE2eFn contains a prepared sendE2E operation and sends an E2E message when
+// called, returning the results of the send.
+type sendE2eFn func() ([]id.Round, e2e.MessageID, time.Time, error)
+
+// prepareSendE2E makes a prepared function that does the e2e send.
+// This is so that when doing deletePartner we can prepare the send before
+// deleting and then send after deleting to ensure there is correctness.
+//
+// Note: the timestamp in the send is recorded in this call, not when the
+// sendE2e function is called.
+func (m *manager) prepareSendE2E(mt catalog.MessageType, recipient *id.ID,
+	payload []byte, params Params) (sendE2E sendE2eFn, err error) {
 	ts := netTime.Now()
 
+	sendFuncs := make([]func(), 0)
+
 	partitions, internalMsgId, err := m.partitioner.Partition(recipient,
 		mt, ts, payload)
 	if err != nil {
-		return nil, e2e.MessageID{}, time.Time{},
-			errors.WithMessage(err, "failed to send unsafe message")
+		return nil, errors.WithMessage(err, "failed to send unsafe message")
 	}
 
 	jww.INFO.Printf("E2E sending %d messages to %s", len(partitions), recipient)
@@ -66,7 +77,7 @@ func (m *manager) sendE2E(mt catalog.MessageType, recipient *id.ID,
 	// key negotiated for the ratchet
 	partner, err := m.Ratchet.GetPartner(recipient)
 	if err != nil {
-		return nil, e2e.MessageID{}, time.Time{}, errors.WithMessagef(err,
+		return nil, errors.WithMessagef(err,
 			"cannot send E2E message no relationship found with %s", recipient)
 	}
 
@@ -101,7 +112,7 @@ func (m *manager) sendE2E(mt catalog.MessageType, recipient *id.ID,
 			keyGetter, params.KeyGetRetryCount, params.KeyGeRetryDelay,
 			params.Stop, recipient, format.DigestContents(p), i)
 		if err != nil {
-			return nil, e2e.MessageID{}, time.Time{}, errors.WithMessagef(err,
+			return nil, errors.WithMessagef(err,
 				"Failed to get key for end-to-end encryption")
 		}
 
@@ -123,36 +134,56 @@ func (m *manager) sendE2E(mt catalog.MessageType, recipient *id.ID,
 
 		// We send each partition in its own thread here; some may send in round
 		// X, others in X+1 or X+2, and so on
-		wg.Add(1)
-		go func(i int) {
-			var err error
-			roundIds[i], _, err = m.net.Send(recipient,
-				key.Fingerprint(), s, contentsEnc, mac, params.CMIXParams)
-			if err != nil {
-				errCh <- err
-			}
-			wg.Done()
-		}(i)
+		localI := i
+		thisSendFunc := func() {
+			wg.Add(1)
+			go func(i int) {
+				var err error
+				roundIds[i], _, err = m.net.Send(recipient,
+					key.Fingerprint(), s, contentsEnc, mac, params.CMIXParams)
+				if err != nil {
+					errCh <- err
+				}
+				wg.Done()
+			}(localI)
+		}
+		sendFuncs = append(sendFuncs, thisSendFunc)
 	}
 
-	wg.Wait()
-
-	numFail, errRtn := getSendErrors(errCh)
-	if numFail > 0 {
-		jww.INFO.Printf("Failed to E2E send %d/%d to %s",
-			numFail, len(partitions), recipient)
-		return nil, e2e.MessageID{}, time.Time{}, errors.Errorf(
-			"Failed to E2E send %v/%v sub payloads: %s",
-			numFail, len(partitions), errRtn)
-	} else {
-		jww.INFO.Printf("Successfully E2E sent %d/%d to %s",
-			len(partitions)-numFail, len(partitions), recipient)
-	}
+	sendE2E = func() ([]id.Round, e2e.MessageID, time.Time, error) {
+		for i := range sendFuncs {
+			sendFuncs[i]()
+		}
 
-	jww.INFO.Printf("Successful E2E Send of %d messages to %s with msgID %s",
-		len(partitions), recipient, msgID)
+		wg.Wait()
 
-	return roundIds, msgID, ts, nil
+		numFail, errRtn := getSendErrors(errCh)
+		if numFail > 0 {
+			jww.INFO.Printf("Failed to E2E send %d/%d to %s",
+				numFail, len(partitions), recipient)
+			return nil, e2e.MessageID{}, time.Time{}, errors.Errorf(
+				"Failed to E2E send %v/%v sub payloads: %s",
+				numFail, len(partitions), errRtn)
+		} else {
+			jww.INFO.Printf("Successfully E2E sent %d/%d to %s",
+				len(partitions)-numFail, len(partitions), recipient)
+		}
+
+		jww.INFO.Printf("Successful E2E Send of %d messages to %s with msgID %s",
+			len(partitions), recipient, msgID)
+
+		return roundIds, msgID, ts, nil
+	}
+	return sendE2E, nil
+}
+
+func (m *manager) sendE2E(mt catalog.MessageType, recipient *id.ID,
+	payload []byte, params Params) ([]id.Round, e2e.MessageID, time.Time, error) {
+	sendFunc, err := m.prepareSendE2E(mt, recipient, payload, params)
+	if err != nil {
+		return nil, e2e.MessageID{}, time.Time{}, err
+	}
+	return sendFunc()
 }
 
 // waitForKey waits the designated amount of time for a key to become available
diff --git a/fileTransfer/e2e/utils_test.go b/fileTransfer/e2e/utils_test.go
index 633483607275fb4e2714ccc67f1852a22c1d78a8..164d155740c789916f1ff5a96b3bd3c89079c61f 100644
--- a/fileTransfer/e2e/utils_test.go
+++ b/fileTransfer/e2e/utils_test.go
@@ -288,24 +288,28 @@ func (m *mockE2e) UnregisterUserListeners(*id.ID) { panic("implement me") }
 func (m *mockE2e) AddPartner(*id.ID, *cyclic.Int, *cyclic.Int, *sidh.PublicKey, *sidh.PrivateKey, session.Params, session.Params) (partner.Manager, error) {
 	panic("implement me")
 }
-func (m *mockE2e) GetPartner(*id.ID) (partner.Manager, error) { panic("implement me") }
-func (m *mockE2e) DeletePartner(*id.ID) error                 { panic("implement me") }
-func (m *mockE2e) GetAllPartnerIDs() []*id.ID                 { panic("implement me") }
-func (m *mockE2e) HasAuthenticatedChannel(*id.ID) bool        { panic("implement me") }
-func (m *mockE2e) AddService(string, message.Processor) error { panic("implement me") }
-func (m *mockE2e) RemoveService(string) error                 { panic("implement me") }
+func (m *mockE2e) GetPartner(*id.ID) (partner.Manager, error)   { panic("implement me") }
+func (m *mockE2e) DeletePartner(*id.ID) error                   { panic("implement me") }
+func (m *mockE2e) DeletePartnerNotify(*id.ID, e2e.Params) error { panic("implement me") }
+func (m *mockE2e) GetAllPartnerIDs() []*id.ID                   { panic("implement me") }
+func (m *mockE2e) HasAuthenticatedChannel(*id.ID) bool          { panic("implement me") }
+func (m *mockE2e) AddService(string, message.Processor) error   { panic("implement me") }
+func (m *mockE2e) RemoveService(string) error                   { panic("implement me") }
 func (m *mockE2e) SendUnsafe(catalog.MessageType, *id.ID, []byte, e2e.Params) ([]id.Round, time.Time, error) {
 	panic("implement me")
 }
-func (m *mockE2e) EnableUnsafeReception()              { panic("implement me") }
-func (m *mockE2e) GetGroup() *cyclic.Group             { panic("implement me") }
-func (m *mockE2e) GetHistoricalDHPubkey() *cyclic.Int  { panic("implement me") }
-func (m *mockE2e) GetHistoricalDHPrivkey() *cyclic.Int { panic("implement me") }
-func (m *mockE2e) GetReceptionID() *id.ID              { panic("implement me") }
-func (m *mockE2e) FirstPartitionSize() uint            { panic("implement me") }
-func (m *mockE2e) SecondPartitionSize() uint           { panic("implement me") }
-func (m *mockE2e) PartitionSize(uint) uint             { panic("implement me") }
-func (m *mockE2e) PayloadSize() uint                   { panic("implement me") }
+func (m *mockE2e) EnableUnsafeReception()                    { panic("implement me") }
+func (m *mockE2e) GetGroup() *cyclic.Group                   { panic("implement me") }
+func (m *mockE2e) GetHistoricalDHPubkey() *cyclic.Int        { panic("implement me") }
+func (m *mockE2e) GetHistoricalDHPrivkey() *cyclic.Int       { panic("implement me") }
+func (m *mockE2e) GetReceptionID() *id.ID                    { panic("implement me") }
+func (m *mockE2e) FirstPartitionSize() uint                  { panic("implement me") }
+func (m *mockE2e) SecondPartitionSize() uint                 { panic("implement me") }
+func (m *mockE2e) PartitionSize(uint) uint                   { panic("implement me") }
+func (m *mockE2e) PayloadSize() uint                         { panic("implement me") }
+func (m *mockE2e) RegisterCallbacks(e2e.Callbacks)           { panic("implement me") }
+func (m *mockE2e) AddPartnerCallbacks(*id.ID, e2e.Callbacks) { panic("implement me") }
+func (m *mockE2e) DeletePartnerCallbacks(*id.ID)             { panic("implement me") }
 
 ////////////////////////////////////////////////////////////////////////////////
 // Mock Storage Session                                                       //
diff --git a/groupChat/e2eManager_test.go b/groupChat/e2eManager_test.go
index cc66b2b7dfd6e0c6b4d489fd4a710974250ddd57..37e4019cb8442cf95c8537cbb412d2d4a5d1e585 100644
--- a/groupChat/e2eManager_test.go
+++ b/groupChat/e2eManager_test.go
@@ -97,6 +97,16 @@ func (*testE2eManager) AddService(string, message.Processor) error {
 // Unused & unimplemented methods of the test object ////////////////////////////////
 /////////////////////////////////////////////////////////////////////////////////////
 
+func (tnm *testE2eManager) DeletePartner(partnerId *id.ID) error {
+	// TODO implement me
+	panic("implement me")
+}
+
+func (tnm *testE2eManager) DeletePartnerNotify(partnerId *id.ID, params clientE2E.Params) error {
+	// TODO implement me
+	panic("implement me")
+}
+
 func (*testE2eManager) GetDefaultHistoricalDHPubkey() *cyclic.Int {
 	panic("implement me")
 }
@@ -130,8 +140,18 @@ func (tnm *testE2eManager) UnregisterUserListeners(userID *id.ID) {
 	panic("implement me")
 }
 
-func (tnm *testE2eManager) DeletePartner(partnerId *id.ID) error {
-	//TODO implement me
+func (tnm *testE2eManager) RegisterCallbacks(callbacks clientE2E.Callbacks) {
+	// TODO implement me
+	panic("implement me")
+}
+
+func (tnm *testE2eManager) AddPartnerCallbacks(partnerID *id.ID, cb clientE2E.Callbacks) {
+	// TODO implement me
+	panic("implement me")
+}
+
+func (tnm *testE2eManager) DeletePartnerCallbacks(partnerID *id.ID) {
+	// TODO implement me
 	panic("implement me")
 }
 
diff --git a/ud/mockE2e_test.go b/ud/mockE2e_test.go
index 3be9c745ab0e44bfe413029872e3c9d28d007df6..28ddaeb0badeab3fd2eb0d18e0cacaba137927e0 100644
--- a/ud/mockE2e_test.go
+++ b/ud/mockE2e_test.go
@@ -99,6 +99,11 @@ func (m mockE2e) GetStorage() storage.Session {
 
 type mockE2eHandler struct{}
 
+func (m mockE2eHandler) RegisterCallbacks(callbacks e2e.Callbacks) {
+	// TODO implement me
+	panic("implement me")
+}
+
 func (m mockE2eHandler) StartProcesses() (stoppable.Stoppable, error) {
 	//TODO implement me
 	panic("implement me")
@@ -145,7 +150,22 @@ func (m mockE2eHandler) GetPartner(partnerID *id.ID) (partner.Manager, error) {
 }
 
 func (m mockE2eHandler) DeletePartner(partnerId *id.ID) error {
-	//TODO implement me
+	// TODO implement me
+	panic("implement me")
+}
+
+func (tnm mockE2eHandler) DeletePartnerNotify(partnerId *id.ID, params e2e.Params) error {
+	// TODO implement me
+	panic("implement me")
+}
+
+func (m mockE2eHandler) AddPartnerCallbacks(partnerID *id.ID, cb e2e.Callbacks) {
+	// TODO implement me
+	panic("implement me")
+}
+
+func (m mockE2eHandler) DeletePartnerCallbacks(partnerID *id.ID) {
+	// TODO implement me
 	panic("implement me")
 }
 
diff --git a/xxdk/e2e.go b/xxdk/e2e.go
index 722195e9aa47fce2726ad01a19f36db3a9738411..4c2b9e22ca2e800e77489f69690092e13f6db6bc 100644
--- a/xxdk/e2e.go
+++ b/xxdk/e2e.go
@@ -320,6 +320,34 @@ func (m *E2e) DeleteContact(partnerId *id.ID) error {
 	return nil
 }
 
+// DeleteContactNotify removes a partner from E2e's storage and sends an E2E
+// message to the contact notifying them.
+func (m *E2e) DeleteContactNotify(partnerId *id.ID, params e2e.Params) error {
+	jww.DEBUG.Printf("Deleting contact with ID %s", partnerId)
+
+	_, err := m.e2e.GetPartner(partnerId)
+	if err != nil {
+		return errors.WithMessagef(err, "Could not delete %s because "+
+			"they could not be found", partnerId)
+	}
+
+	if err = m.e2e.DeletePartnerNotify(partnerId, params); err != nil {
+		return err
+	}
+
+	m.backup.TriggerBackup("contact deleted")
+
+	// FIXME: Do we need this?
+	// c.e2e.Conversations().Delete(partnerId)
+
+	// call delete requests to make sure nothing is lingering.
+	// this is for safety to ensure the contact can be re-added
+	// in the future
+	_ = m.auth.DeleteRequest(partnerId)
+
+	return nil
+}
+
 // MakeAuthCallbacksAdapter creates an authCallbacksAdapter.
 func MakeAuthCallbacksAdapter(ac AuthCallbacks, e2e *E2e) *authCallbacksAdapter {
 	return &authCallbacksAdapter{