This is just a sample app done in a couple of days to showcase my python coding.
EDIT: I'm actually using it in production now :)
Problem, solution and design described at the end of the readme.
- Python3: tested with 3.5, 3.6
- Bottle:: Straight forward, no boilerplate
- Python packages: email-validator, requests, pyYaml, simple-crypt
I tried to keep the dependencies to a minimum.
- For deployement: bottle is running gunicorn
- For testing: tox, pylint and py.test (but tests are written with the std lib so you can pick another runner like nose)
A demo app is running on: http://email.marech.fr/ (ubuntu-server) Apache is running as reverse proxy in front. / is redirecting you to the doc for now. You can try the endpoint from there.
Docs are generated by swagger, the see swagger.yaml
Deployed with node.js. The swagger server is not included in the Repo you can generate your own with swagger codegen
http://email.marech.fr/doc/docs/#!/send/emailPOST (The API is offline, contact me if you'd like it back online)
From the root
pip install tox
tox
If you run into troubles after modifying the code of moving the repo:
tox --recreate
If pylint is complaining about something silly and you think it does not make sense you can edit:
.pylintrc
Or rather add a # pylint: disable=XXX
inline in the code
From the root of the repo, or after installing with:
mkvirtualenv test_api -p python3 --no-site-packages
workon test_api
python setup.py install
Run:
EMAIL_API_CONFIG=path/to/config.yaml python -m email_api.api
You must add your own credentials in the config.yaml as well as your domains:
host: localhost
port: 8080
server: gunicorn # remove for default server
workers: 4 # remove for default server
# You cam put fake value here but not email will be sent
providers:
sendgrid:
user: # add your user
key: # add your key
mailgun:
user: 'api'
key: # add your key
domain: 'foo.bar'
from_name: noreply
human_name: My App Name
elasticemail:
user: # add your user
key:
key: # add your key
domain: 'bar.foo'
from_name: noreply
human_name: My App Name
You can configure routes based on recipients, if the regex matches the providers listed will be used instead of default order: The route type must be specified in the POST data when calling the API
routes:
default:
- elasticemail
- mailgun
- sendgrid
recipients:
- regex: '.*@((hotmail)|(outlook)|(live))\..*'
providers:
- mailgun
- elasticemail
Once the server is running you can start shooting emails:
pip install httpie
http post email.marech.fr/email "to=mail@mail.com"
http post http://localhost:8080/email "to=juju <mw@blah.fr>" "subject=Hello" "html=<a href="https://google.fr">blah</a>" "route=recipients"
http post email.marech.fr/email "to=mail@mail.com" "subject=yeyeyey" "text=abc"
http post email.marech.fr/email "to=juju <mail@mail.com>" "subject=" "text=" "cc=mail2@mail2.fr"
http post email.marech.fr/email 'to:=["mail@mail.com","blah@t.com"]' 'body:=["mail@mail.com","mail2@mail2.fr"]'
http post email.marech.fr/email "to=juju <mail@mail.com>" "subject=hello" "body=bam" "cc=blah@toto.com" "from=Hey You <hey@blah.com>" "reply*to=mail@mail.com"
Note: The API is offline, contact me if you'd like it back online
Don't abuse it too much, there's a quota!
We want to be able to send emails through external third party APIs, without having to think which one we should use, if it's up or not, and how to format the data.
We provide ONE API that serves as a facade and is able to communicate with several providers, transparently, falling back to the next provider if the previous one failed. The solution is ideally decoupled from the web framework, and can onboard new providers with minimal coding requirements.
- Define core data structures that are framework independent and enforce the main business rules and use cases. See
message.py
- Define an abstraction for a provider, the class specialization should only contain 'pure functions' meaning data:in -> data:out, no side effects, no I/Os, just taking the core structures and returning their API specific format as well as HTTP method/auth required to make a request. See
abstract_provider.py
- Create a class that knows how to handle an abstract provider. See
providers_manager.py
.- The manager serves as Factory for providers and as a Facade to handle actual HTTP requests (These two concerns could be separeted if the code is to grow).
- Provided with a list of registered providers classes, the manager will, one by one:
- instanciate the provider
- collect the required data
- make the HTTP request
- stop on success
- go to the next provider on failure
- The manager has to be as bullet-proof as possible and be protected against bad Provider implementation/configuration, http/socket errors etc..
- The dependency on the HTTP library should also be limited in scope for easy replacement.
- The web framework just glues the above together and should be kept thin:
- Unpack HTTP/JSON parameters
- Build Email structure
- Catch invalid business use cases, serialize error to json
- Pass the Email to the Manager
- Return manager's result to client
Some nice features I would have liked to add with more time on my hands:
- Make the Email persistent (sent or not), using SQLAchemy and SQLite (for starters), adding an ID and status. (Normalized records of recipients would be nice too)
- Add a GET endpoint
email/
andemail/<id>
rescpectively showing all (paginated) recorded emails and one email by ID - Add a POST
webohook/<providername>
endpoint for recording callbacks from providers and updating Email records' status. Would be rather easy to add with the current skeleton, but useless without persistence. - Add HATEOAS links in the return of endpoints, e.g POST /email -> GET /email/1
- Add PATCH '/email' to amend an invalid email that was not sent.
- Add rate limiting and auth
- Add proper integration tests, It's a bit tricky when dealing with emails, I need to make some more research
- Remove the default hardcoded config and the encrypted keys from the code, there're just here for convenience.
- Configure the Logger properly