about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound')
-rw-r--r--.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/__init__.py13
-rw-r--r--.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/app.py45
-rw-r--r--.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/config.py65
-rw-r--r--.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/config.yml34
-rw-r--r--.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/parse.py100
-rw-r--r--.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/default_data.txt58
-rw-r--r--.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/default_data_with_attachments.txt0
-rw-r--r--.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/raw_data.txt57
-rw-r--r--.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/raw_data_with_attachments.txt298
-rw-r--r--.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/send.py61
10 files changed, 731 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/__init__.py b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/__init__.py
new file mode 100644
index 00000000..85ab4d0b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/__init__.py
@@ -0,0 +1,13 @@
+"""
+Inbound Parse helper
+--------------------
+This is a standalone module to help get you started consuming and processing
+Inbound Parse data.  It provides a Flask server to listen for Inbound Parse
+POSTS, and utilities to send sample data to the server.
+
+See README.txt for detailed usage instructions, including quick-start guides
+for local testing and Heroku deployment.
+"""
+
+from .config import *  # noqa
+from .parse import *  # noqa
diff --git a/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/app.py b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/app.py
new file mode 100644
index 00000000..0d443590
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/app.py
@@ -0,0 +1,45 @@
+"""Receiver module for processing SendGrid Inbound Parse messages.
+
+See README.txt for usage instructions."""
+try:
+    from config import Config
+except:
+    # Python 3+, Travis
+    from sendgrid.helpers.inbound.config import Config
+
+try:
+    from parse import Parse
+except:
+    # Python 3+, Travis
+    from sendgrid.helpers.inbound.parse import Parse
+
+from flask import Flask, request, render_template
+import os
+
+app = Flask(__name__)
+config = Config()
+
+
+@app.route('/', methods=['GET'])
+def index():
+    """Show index page to confirm that server is running."""
+    return render_template('index.html')
+
+
+@app.route(config.endpoint, methods=['POST'])
+def inbound_parse():
+    """Process POST from Inbound Parse and print received data."""
+    parse = Parse(config, request)
+    # Sample processing action
+    print(parse.key_values())
+    # Tell SendGrid's Inbound Parse to stop sending POSTs
+    # Everything is 200 OK :)
+    return "OK"
+
+
+if __name__ == '__main__':
+    # Be sure to set config.debug_mode to False in production
+    port = int(os.environ.get("PORT", config.port))
+    if port != config.port:
+        config.debug = False
+    app.run(host='0.0.0.0', debug=config.debug_mode, port=port)
diff --git a/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/config.py b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/config.py
new file mode 100644
index 00000000..06ca683c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/config.py
@@ -0,0 +1,65 @@
+"""Set up credentials (.env) and application variables (config.yml)"""
+import os
+import yaml
+
+
+class Config(object):
+    """All configuration for this app is loaded here"""
+
+    def __init__(self, **opts):
+        if os.environ.get('ENV') != 'prod':  # We are not in Heroku
+            self.init_environment()
+
+        """Allow variables assigned in config.yml available the following variables
+           via properties"""
+        self.path = opts.get(
+            'path', os.path.abspath(os.path.dirname(__file__))
+        )
+        with open('{0}/config.yml'.format(self.path)) as stream:
+            config = yaml.load(stream, Loader=yaml.FullLoader)
+            self._debug_mode = config['debug_mode']
+            self._endpoint = config['endpoint']
+            self._host = config['host']
+            self._keys = config['keys']
+            self._port = config['port']
+
+    @staticmethod
+    def init_environment():
+        """Allow variables assigned in .env available using
+           os.environ.get('VAR_NAME')"""
+        base_path = os.path.abspath(os.path.dirname(__file__))
+        env_path = '{0}/.env'.format(base_path)
+        if os.path.exists(env_path):
+            with open(env_path) as f:
+                lines = f.readlines()
+                for line in lines:
+                    var = line.strip().split('=')
+                    if len(var) == 2:
+                        os.environ[var[0]] = var[1]
+
+    @property
+    def debug_mode(self):
+        """Flask debug mode - set to False in production."""
+        return self._debug_mode
+
+    @property
+    def endpoint(self):
+        """Endpoint to receive Inbound Parse POSTs."""
+        return self._endpoint
+
+    @property
+    def host(self):
+        """URL that the sender will POST to."""
+        return self._host
+
+    @property
+    def keys(self):
+        """Incoming Parse fields to parse. For reference, see
+        https://sendgrid.com/docs/Classroom/Basics/Inbound_Parse_Webhook/setting_up_the_inbound_parse_webhook.html
+        """
+        return self._keys
+
+    @property
+    def port(self):
+        """Port to listen on."""
+        return self._port
diff --git a/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/config.yml b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/config.yml
new file mode 100644
index 00000000..d1a131ae
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/config.yml
@@ -0,0 +1,34 @@
+# Incoming Parse endpoint
+endpoint: '/inbound'
+
+# Port to listen on
+port: 5000
+
+# Flask debug mode
+# Set this to False in production
+# Reference: http://flask.pocoo.org/docs/0.11/api/#flask.Flask.run
+debug_mode: True
+
+# List all Incoming Parse fields you would like parsed
+# Reference: https://sendgrid.com/docs/Classroom/Basics/Inbound_Parse_Webhook/setting_up_the_inbound_parse_webhook.html
+keys:
+ - from
+ - attachments
+ - headers
+ - text
+ - envelope
+ - to
+ - html
+ - sender_ip
+ - attachment-info
+ - subject
+ - dkim
+ - SPF
+ - charsets
+ - content-ids
+ - spam_report
+ - spam_score
+ - email
+
+# URL that the sender will POST to
+host: 'http://127.0.0.1:5000/inbound'
diff --git a/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/parse.py b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/parse.py
new file mode 100644
index 00000000..49627a12
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/parse.py
@@ -0,0 +1,100 @@
+"""Parse data received from the SendGrid Inbound Parse webhook"""
+import base64
+import email
+import mimetypes
+from six import iteritems
+from werkzeug.utils import secure_filename
+
+
+class Parse(object):
+
+    def __init__(self, config, request):
+        self._keys = config.keys
+        self._request = request
+        request.get_data(as_text=True)
+        self._payload = request.form
+        self._raw_payload = request.data
+
+    def key_values(self):
+        """
+        Return a dictionary of key/values in the payload received from
+        the webhook
+        """
+        key_values = {}
+        for key in self.keys:
+            if key in self.payload:
+                key_values[key] = self.payload[key]
+        return key_values
+
+    def get_raw_email(self):
+        """
+        This only applies to raw payloads:
+        https://sendgrid.com/docs/Classroom/Basics/Inbound_Parse_Webhook/setting_up_the_inbound_parse_webhook.html#-Raw-Parameters
+        """
+        if 'email' in self.payload:
+            raw_email = email.message_from_string(self.payload['email'])
+            return raw_email
+        else:
+            return None
+
+    def attachments(self):
+        """Returns an object with:
+        type = file content type
+        file_name = the name of the file
+        contents = base64 encoded file contents"""
+        attachments = None
+        if 'attachment-info' in self.payload:
+            attachments = self._get_attachments(self.request)
+        # Check if we have a raw message
+        raw_email = self.get_raw_email()
+        if raw_email is not None:
+            attachments = self._get_attachments_raw(raw_email)
+        return attachments
+
+    def _get_attachments(self, request):
+        attachments = []
+        for _, filestorage in iteritems(request.files):
+            attachment = {}
+            if filestorage.filename not in (None, 'fdopen', '<fdopen>'):
+                filename = secure_filename(filestorage.filename)
+                attachment['type'] = filestorage.content_type
+                attachment['file_name'] = filename
+                attachment['contents'] = base64.b64encode(filestorage.read())
+                attachments.append(attachment)
+        return attachments
+
+    def _get_attachments_raw(self, raw_email):
+        attachments = []
+        counter = 1
+        for part in raw_email.walk():
+            attachment = {}
+            if part.get_content_maintype() == 'multipart':
+                continue
+            filename = part.get_filename()
+            if not filename:
+                ext = mimetypes.guess_extension(part.get_content_type())
+                if not ext:
+                    ext = '.bin'
+                filename = 'part-%03d%s' % (counter, ext)
+            counter += 1
+            attachment['type'] = part.get_content_type()
+            attachment['file_name'] = filename
+            attachment['contents'] = part.get_payload(decode=False)
+            attachments.append(attachment)
+        return attachments
+
+    @property
+    def keys(self):
+        return self._keys
+
+    @property
+    def request(self):
+        return self._request
+
+    @property
+    def payload(self):
+        return self._payload
+
+    @property
+    def raw_payload(self):
+        return self._raw_payload
diff --git a/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/default_data.txt b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/default_data.txt
new file mode 100644
index 00000000..7c3ce6be
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/default_data.txt
@@ -0,0 +1,58 @@
+--xYzZY
+Content-Disposition: form-data; name="headers"
+
+MIME-Version: 1.0
+Received: by 0.0.0.0 with HTTP; Wed, 10 Aug 2016 18:10:13 -0700 (PDT)
+From: Example User <test@example.com>
+Date: Wed, 10 Aug 2016 18:10:13 -0700
+Subject: Inbound Parse Test Data
+To: inbound@inbound.example.com
+Content-Type: multipart/alternative; boundary=001a113df448cad2d00539c16e89
+
+--xYzZY
+Content-Disposition: form-data; name="dkim"
+
+{@sendgrid.com : pass}
+--xYzZY
+Content-Disposition: form-data; name="to"
+
+inbound@inbound.example.com
+--xYzZY
+Content-Disposition: form-data; name="html"
+
+<html><body><strong>Hello Twilio SendGrid!</body></html>
+
+--xYzZY
+Content-Disposition: form-data; name="from"
+
+Example User <test@example.com>
+--xYzZY
+Content-Disposition: form-data; name="text"
+
+Hello Twilio SendGrid!
+
+--xYzZY
+Content-Disposition: form-data; name="sender_ip"
+
+0.0.0.0
+--xYzZY
+Content-Disposition: form-data; name="envelope"
+
+{"to":["inbound@inbound.example.com"],"from":"test@example.com"}
+--xYzZY
+Content-Disposition: form-data; name="attachments"
+
+0
+--xYzZY
+Content-Disposition: form-data; name="subject"
+
+Testing non-raw
+--xYzZY
+Content-Disposition: form-data; name="charsets"
+
+{"to":"UTF-8","html":"UTF-8","subject":"UTF-8","from":"UTF-8","text":"UTF-8"}
+--xYzZY
+Content-Disposition: form-data; name="SPF"
+
+pass
+--xYzZY--
\ No newline at end of file
diff --git a/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/default_data_with_attachments.txt b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/default_data_with_attachments.txt
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/default_data_with_attachments.txt
diff --git a/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/raw_data.txt b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/raw_data.txt
new file mode 100644
index 00000000..12f64cb4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/raw_data.txt
@@ -0,0 +1,57 @@
+--xYzZY
+Content-Disposition: form-data; name="dkim"
+
+{@sendgrid.com : pass}
+--xYzZY
+Content-Disposition: form-data; name="email"
+
+MIME-Version: 1.0
+Received: by 0.0.0.0 with HTTP; Wed, 10 Aug 2016 14:44:21 -0700 (PDT)
+From: Example User <test@example.com>
+Date: Wed, 10 Aug 2016 14:44:21 -0700
+Subject: Inbound Parse Test Raw Data
+To: inbound@inbound.inbound.com
+Content-Type: multipart/alternative; boundary=001a113ee97c89842f0539be8e7a
+
+--001a113ee97c89842f0539be8e7a
+Content-Type: text/plain; charset=UTF-8
+
+Hello Twilio SendGrid!
+
+--001a113ee97c89842f0539be8e7a
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+<html><body><strong>Hello Twilio SendGrid!</body></html>
+
+--001a113ee97c89842f0539be8e7a--
+
+--xYzZY
+Content-Disposition: form-data; name="to"
+
+inbound@inbound.inbound.com
+--xYzZY
+Content-Disposition: form-data; name="from"
+
+Example User <test@example.com>
+--xYzZY
+Content-Disposition: form-data; name="sender_ip"
+
+0.0.0.0
+--xYzZY
+Content-Disposition: form-data; name="envelope"
+
+{"to":["inbound@inbound.inbound.com"],"from":"test@example.com"}
+--xYzZY
+Content-Disposition: form-data; name="subject"
+
+Testing with Request.bin
+--xYzZY
+Content-Disposition: form-data; name="charsets"
+
+{"to":"UTF-8","subject":"UTF-8","from":"UTF-8"}
+--xYzZY
+Content-Disposition: form-data; name="SPF"
+
+pass
+--xYzZY--
\ No newline at end of file
diff --git a/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/raw_data_with_attachments.txt b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/raw_data_with_attachments.txt
new file mode 100644
index 00000000..058fd8a6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/sample_data/raw_data_with_attachments.txt
@@ -0,0 +1,298 @@
+--xYzZY
+Content-Disposition: form-data; name="dkim"
+
+{@sendgrid.com : pass}
+--xYzZY
+Content-Disposition: form-data; name="email"
+
+MIME-Version: 1.0
+Received: by 0.0.0.0 with HTTP; Mon, 15 Aug 2016 13:47:21 -0700 (PDT)
+From: Example User <test@example.com>
+Date: Mon, 15 Aug 2016 13:47:21 -0700
+Subject: Inbound Parse Test Raw Data with Attachment
+To: inbound@inbound.inbound.com
+Content-Type: multipart/mixed; boundary=001a1140ffb6f4fc63053a2257e2
+
+--001a1140ffb6f4fc63053a2257e2
+Content-Type: multipart/alternative; boundary=001a1140ffb6f4fc5f053a2257e0
+
+--001a1140ffb6f4fc5f053a2257e0
+Content-Type: text/plain; charset=UTF-8
+
+Hello Twilio SendGrid!
+
+--001a1140ffb6f4fc5f053a2257e0
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+<html><body><strong>Hello Twilio SendGrid!</body></html>
+
+--001a1140ffb6f4fc5f053a2257e0--
+
+--001a1140ffb6f4fc63053a2257e2
+Content-Type: image/jpeg; name="SendGrid.jpg"
+Content-Disposition: attachment; filename="SendGrid.jpg"
+Content-Transfer-Encoding: base64
+X-Attachment-Id: f_irwihell0
+
+/9j/4AAQSkZJRgABAQABLAEsAAD/4QDKRXhpZgAATU0AKgAAAAgABwESAAMA
+AAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAEx
+AAIAAAARAAAAcgEyAAIAAAAUAAAAhIdpAAQAAAABAAAAmAAAAAAAAAEsAAAA
+AQAAASwAAAABUGl4ZWxtYXRvciAzLjQuNAAAMjAxNjowODoxMSAxNjowODo1
+NwAAA6ABAAMAAAABAAEAAKACAAQAAAABAAACEqADAAQAAAABAAACFQAAAAD/
+4Qn2aHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVn
+aW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4
+bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAg
+Q29yZSA1LjQuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53
+My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3Jp
+cHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2Jl
+LmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9l
+bGVtZW50cy8xLjEvIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxNi0wOC0xMVQxNjow
+ODo1NyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBJbWFnZVJlYWR5Ij4gPGRj
+OnN1YmplY3Q+IDxyZGY6QmFnLz4gPC9kYzpzdWJqZWN0PiA8L3JkZjpEZXNj
+cmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICA8P3hwYWNrZXQgZW5kPSJ3Ij8+AP/tADhQaG90b3Nob3Ag
+My4wADhCSU0EBAAAAAAAADhCSU0EJQAAAAAAENQdjNmPALIE6YAJmOz4Qn7/
+wAARCAIVAhIDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQF
+BgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJx
+FDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdI
+SUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKj
+pKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx
+8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QA
+tREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHB
+CSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldY
+WVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmq
+srO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6
+/9sAQwAcHBwcHBwwHBwwRDAwMERcRERERFx0XFxcXFx0jHR0dHR0dIyMjIyM
+jIyMqKioqKioxMTExMTc3Nzc3Nzc3Nzc/9sAQwEiJCQ4NDhgNDRg5pyAnObm
+5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm
+5ubm/90ABAAi/9oADAMBAAIRAxEAPwDpKKKKACiiigAooooAKKKKACiiigAo
+oooAKKKKACiiigAooooAKKKKACiiigDEnkkEzgMep71F5sn94/nTp/8AXP8A
+U1FXWloeZJu7H+bJ/eP50ebJ/eP50yinYm7H+bJ/eP50ebJ/eP50yiiwXY/z
+ZP7x/OjzZP7x/OmUUWC7H+bJ/eP50ebJ/eP50yiiwXY/zZP7x/OjzZP7x/Om
+UUWC7H+bJ/eP50ebJ/eP50yiiwXY/wA2T+8fzo82T+8fzplFFgux/myf3j+d
+Hmyf3j+dMoosF2P82T+8fzo82T+8fzplFFgux/myf3j+dHmyf3j+dMoosF2I
+0sufvt+dJ5sv99vzNMbrSVVkbpuxJ5sv99vzNHmy/wB9vzNR0U7Id2SebL/f
+b8zR5sv99vzNR0UWQXZJ5sv99vzNHmy/32/M1HRRZBdknmy/32/M0ebL/fb8
+zUdFFkF2SebL/fb8zR5sv99vzNR0UWQXZJ5sv99vzNHmy/32/M1HRRZBdknm
+y/32/M0ebL/fb8zUdFFkF2SebL/fb8zR5sv99vzNR0UWQXZJ5sv99vzNHmy/
+32/M1HRRZBdknmy/32/M0ebL/fb8zUdFFkF2SebL/fb8zR5sv99vzNR0UWQX
+Z//Q6SiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAo
+oooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKACiiigAooooAKK
+KKACiiigAooooAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiigAooooAKKKKAC
+iiigAooooAKKKKACiiigAooooAKKKKACiiigD//R6SiiigAooooAKKKKACii
+igAooooAKKKKACiiigAooooAKKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKu
+xbHly3YUUUUyQooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig
+CJutJSt1pKo3WwUUUUDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooo
+oAKKKKACiiigD//S6SiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA
+KKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKAC
+iiigAooooAKKKKACiiigAooooAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiig
+AooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//T6SiiigAo
+oooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAwZ/9c/1N
+RVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKACiiigAooooAKKKKACiiigAooo
+oAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiigAooooAKKKKACiiigAooooAKK
+KKACiiigAooooAKKKKACiiigD//U6SiiigAooooAKKKKACiiigAooooAKKKK
+ACiiigAooooAKKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUy
+QooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCJutJSt1pKo3
+WwUUUUDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig
+D//V6SiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAo
+oooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKACiiigAooooAKK
+KKACiiigAooooAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiigAooooAKKKKAC
+iiigAooooAKKKKACiiigAooooAKKKKACiiigD//W6SiiigAooooAKKKKACii
+igAooooAKKKKACiiigAooooAKKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKu
+xbHly3YUUUUyQooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig
+CJutJSt1pKo3WwUUUUDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooo
+oAKKKKACiiigD//X6SiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA
+KKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKAC
+iiigAooooAKKKKACiiigAooooAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiig
+AooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//Q6SiiigAo
+oooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAwZ/9c/1N
+RVLP/rn+pqKuxbHly3YUUUUyQooooAKKKKACiiigAooooAKKKKACiiigAooo
+oAKKKKACiiigCJutJSt1pKo3WwUUUUDCiiigAooooAKKKKACiiigAooooAKK
+KKACiiigAooooAKKKKACiiigD//R6SiiigAooooAKKKKACiiigAooooAKKKK
+ACiiigAooooAKKKKACiiigAooooAwZ/9c/1NRVLP/rn+pqKuxbHly3YUUUUy
+QooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigCJutJSt1pKo3
+WwUUUUDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig
+D//S2i7560m9/WmnqaStbHPdj97+tG9/WmUUWC7H739aN7+tMoosF2P3v60b
+39aZRRYLsfvf1o3v60yiiwXY/e/rRvf1plFFgux+9/Wje/rTKKLBdj97+tG9
+/WmUUWC7H739aN7+tMoosF2P3v60b39aZRRYLsfvf1o3v60yiiwXZlzEmVvr
+UeTT5f8AWt9ajrdbHI9xcmjJpKKYhcmjJpKKAFyaMmkooAXJoyaSigBcmjJp
+KKAFyaMmkooAXJoyaSigBcmjJpKKAFyaMmkooAXJoyaSigBwAPWlwKB0paBX
+YmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAX
+YmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAXYmBRgUtFAXZ//9PXPU0l
+KeppK1OYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKK
+AMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFF
+FABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACiiigAooooAKKKKACiiigAoooo
+AKKKKACiiigAooooAKKKKACiiigD/9TXPU0lKeppK1OYKKKKACiiigAooooA
+KKKKACiiigAooooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyv
+cKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQ
+dKWgkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi
+iigD/9XXPU0lKeppK1OYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACii
+igAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABR
+RRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACiiigAooooAKKK
+KACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9bXPU0lKeppK1OYKKKK
+ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqS
+X/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU
+AFFFFADx0paQdKWgkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigA
+ooooAKKKKACiiigD/9fXPU0lKeppK1OYKKKKACiiigAooooAKKKKACiiigAo
+oooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQA
+UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACi
+iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9DXPU0l
+KeppK1OYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKK
+AMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFF
+FABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACiiigAooooAKKKKACiiigAoooo
+AKKKKACiiigAooooAKKKKACiiigD/9HXPU0lKeppK1OYKKKKACiiigAooooA
+KKKKACiiigAooooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyv
+cKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQ
+dKWgkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi
+iigD/9LXPU0lKeppK1OYKKKKACiiigAooooAKKKKACiiigAooooAKKKKACii
+igAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABR
+RRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACiiigAooooAKKK
+KACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9PXPU0lKeppK1OYKKKK
+ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqS
+X/Wt9ajroRyvcKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU
+AFFFFADx0paQdKWgkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigA
+ooooAKKKKACiiigD/9TXPU0lKeppK1OYKKKKACiiigAooooAKKKKACiiigAo
+oooAKKKKACiiigAooooAKKKKAMqX/Wt9ajqSX/Wt9ajroRyvcKKKKBBRRRQA
+UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADx0paQdKWgkKKKKACi
+iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9WV7mUO
+Rnv6U37TN6/pUMn32+pptdyiuxxNssfaZvX9KPtM3r+lV6KOVdguyx9pm9f0
+o+0zev6VXoo5V2C7LH2mb1/Sj7TN6/pVeijlXYLssfaZvX9KPtM3r+lV6KOV
+dguyx9pm9f0o+0zev6VXoo5V2C7LH2mb1/Sj7TN6/pVeijlXYLssfaZvX9KP
+tM3r+lV6KOVdguyx9pm9f0o+0zev6VXoo5V2C7LH2mb1/Sj7TN6/pVeijlXY
+LssfaZvX9KPtM3r+lV6KOVdguzRSFJFEjdTyad9mi96fD/ql+lS1g27lcqK/
+2aL3o+zRe9WKKXMw5V2K/wBmi96Ps0XvViijmYcq7Ff7NF70fZoverFFHMw5
+V2K/2aL3o+zRe9WKKOZhyrsV/s0XvR9mi96sUUczDlXYr/Zovej7NF71Yoo5
+mHKuxX+zRe9H2aL3qxRRzMOVdiv9mi96Ps0XvViijmYcq7Ff7NF70fZoverF
+FHMw5V2K/wBmi96Ps0XvViijmYcq7BHZwlcnP50/7FB6H86ni+4KkqHJ9zdU
+422Kn2KD0P50fYoPQ/nVuilzvuP2cexU+xQeh/Oj7FB6H86t0Uc77h7OPYqf
+YoPQ/nR9ig9D+dW6KOd9w9nHsVPsUHofzo+xQeh/OrdFHO+4ezj2Kn2KD0P5
+0fYoPQ/nVuijnfcPZx7FT7FB6H86PsUHofzq3RRzvuHs49ip9ig9D+dH2KD0
+P51boo533D2cexU+xQeh/Oj7FB6H86t0Uc77h7OPYqfYoPQ/nR9ig9D+dW6K
+Od9w9nHsVPsUHofzo+xQeh/OrdFHO+4ezj2Kn2KD0P50fYoPQ/nVuijnfcPZ
+x7H/1mSffb6mm06T77fU02u9HCwooooAKKKKACiiigAooooAKKKKACiiigAo
+oooAKKKKACiiigAooooA1of9Uv0qWoof9Uv0qWuZ7mqCiiikAUUUUAFFFFAB
+RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBai+4KkqOL7gqSs3udEdgooo
+pDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9dk
+n32+pptOk++31NNrvRwsKKKKACiiigAooooAKKKKACiiigAooooAKKKKACii
+igAooooAKKKKANaH/VL9KlqKH/VL9Klrme5qgooopAFFFFABRRRQAUUUUAFF
+FFABRRRQAUUUUAFFFFABRRRQAUUUUAWovuCpKji+4KkrN7nRHYKKKKQwoooo
+AKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//QZJ99vqab
+TpPvt9TTa70cLCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKK
+ACiiigDWh/1S/Spaih/1S/Spa5nuaoKKKKQBRRRQAUUUUAFFFFABRRRQAUUU
+UAFFFFABRRRQAUUUUAFFFFAFqL7gqSo4vuCpKze50R2CiiikMKKKKACiiigA
+ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/0WSffb6mm06T77fU
+02u9HCwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA
+1of9Uv0qWoof9Uv0qWuZ7mqCiiikAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQ
+AUUUUAFFFFABRRRQBai+4KkqOL7gqSs3udEdgooopDCiiigAooooAKKKKACi
+iigAooooAKKKKACiiigAooooAKKKKACiiigD/9Jkn32+pptOk++31NNrvRws
+KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKANaH/VL9
+KlqKH/VL9Klrme5qgooopAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAB
+RRRQAUUUUAWovuCpKji+4KkrN7nRHYKKKKQwooooAKKKKACiiigAooooAKKK
+KACiiigAooooAKKKKACiiigAooooA//TZJ99vqabTpPvt9TTa70cLCiiigAo
+oooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDWh/1S/Spaih/1
+S/Spa5nuaoKKKKQBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFF
+FFAFqL7gqSo4vuCpKze50R2CiiikMKKKKACiiigAooooAKKKKACiiigAoooo
+AKKKKACiiigAooooAKKKKAP/1GSffb6mm06T77fU02u9HCwooooAKKKKACii
+igAooooAKKKKACiiigAooooAKKKKACiiigAooooA1of9Uv0qWoof9Uv0qWuZ
+7mqCiiikAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBai+
+4KkqOL7gqSs3udEdgooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigA
+ooooAKKKKACiiigD/9Vkn32+pptOk++31NNrvRwsKKKKACiiigAooooAKKKK
+ACiiigAooooAKKKKACiiigAooooAKKKKANaH/VL9KlqKH/VL9Klrme5qgooo
+pAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAWovuCpKji+
+4KkrN7nRHYKKKKQwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi
+iigAooooA//WZJ99vqabTpPvt9TTa70cLCiiigAooooAKKKKACiiigAooooA
+KKKKACiiigAooooAKKKKACiiigDWh/1S/Spaih/1S/Spa5nuaoKKKKQBRRRQ
+AUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAFqL7gqSo4vuCpKze5
+0R2CiiikMKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKK
+KAP/12Sffb6mm06T77fU02u9HCwooooAKKKKACiiigAooooAKKKKACiiigAo
+oooAKKKKACiiigAooooA1of9Uv0qWoof9Uv0qWuZ7mqCiiikAUUUUAFFFFAB
+RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBai+4KkqOL7gqSs3udEdgooo
+pDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9DU
+OnxMSdx5pP7Oi/vNWjRV+0l3I5I9jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s
+6L+81H9nRf3mrRoo9pLuHs49jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s6L+8
+1H9nRf3mrRoo9pLuHs49jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s6L+81H9n
+Rf3mrRoo9pLuHs49jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s6L+81H9nRf3m
+rRoo9pLuHs49jO/s6L+81H9nRf3mrRoo9pLuHs49jO/s6L+81H9nRf3mrRoo
+9pLuHs49iBLdUUKCeKd5K+pqWip5mPlRF5K+po8lfU1LRRdhyoi8lfU0eSvq
+aloouw5UReSvqaPJX1NS0UXYcqIvJX1NHkr6mpaKLsOVEXkr6mjyV9TUtFF2
+HKiLyV9TR5K+pqWii7DlRF5K+po8lfU1LRRdhyoi8lfU0eSvqaloouw5UReS
+vqaPJX1NS0UXYcqIvJX1NHkr6mpaKLsOVCKu0YFLRRSKCiiigAooooAKKKKA
+CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9k=
+
+--001a1140ffb6f4fc63053a2257e2--
+
+--xYzZY
+Content-Disposition: form-data; name="to"
+
+inbound@inbound.inbound.com
+--xYzZY
+Content-Disposition: form-data; name="from"
+
+Example User <test@example.com>
+--xYzZY
+Content-Disposition: form-data; name="sender_ip"
+
+0.0.0.0
+--xYzZY
+Content-Disposition: form-data; name="envelope"
+
+{"to":["inbound@inbound.inbound.com"],"from":"test@example.com"}
+--xYzZY
+Content-Disposition: form-data; name="subject"
+
+Raw Payload
+--xYzZY
+Content-Disposition: form-data; name="charsets"
+
+{"to":"UTF-8","subject":"UTF-8","from":"UTF-8"}
+--xYzZY
+Content-Disposition: form-data; name="SPF"
+
+pass
+--xYzZY--
\ No newline at end of file
diff --git a/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/send.py b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/send.py
new file mode 100644
index 00000000..8dbfa68d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sendgrid/helpers/inbound/send.py
@@ -0,0 +1,61 @@
+"""A module for sending test SendGrid Inbound Parse messages.
+Usage: ./send.py [path to file containing test data]"""
+import argparse
+import sys
+from io import open
+try:
+    from config import Config
+except ImportError:
+    # Python 3+, Travis
+    from sendgrid.helpers.inbound.config import Config
+from python_http_client import Client
+
+
+class Send(object):
+
+    def __init__(self, url):
+        """Create a Send object with target `url`."""
+        self._url = url
+
+    def test_payload(self, payload_filepath):
+        """Send a test payload.
+
+        Load a payload from payload_filepath, apply headers, and POST self.url.
+        Return the response object.
+        """
+        headers = {
+            "User-Agent": "SendGrid-Test",
+            "Content-Type": "multipart/form-data; boundary=xYzZY"
+        }
+        client = Client(host=self.url, request_headers=headers)
+        f = open(payload_filepath, 'r', encoding='utf-8')
+        data = f.read()
+        return client.post(request_body=data)
+
+    @property
+    def url(self):
+        """URL to send to."""
+        return self._url
+
+
+def main():
+    config = Config()
+    parser = argparse.ArgumentParser(
+        description='Test data and optional host.')
+    parser.add_argument('data',
+                        type=str,
+                        help='path to the sample data')
+    parser.add_argument('-host',
+                        type=str,
+                        help='name of host to send the sample data to',
+                        default=config.host, required=False)
+    args = parser.parse_args()
+    send = Send(args.host)
+    response = send.test_payload(sys.argv[1])
+    print(response.status_code)
+    print(response.headers)
+    print(response.body)
+
+
+if __name__ == '__main__':
+    main()