Skip to content

A simple, minimal email API using third party APIs

Notifications You must be signed in to change notification settings

MarechJ/email_api

Repository files navigation

Email Service

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.

Tech stack and libs

  • 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.

API Docs

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)

Tests

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

Usage

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!

Problem definition

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.

Solution

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.

Design

  • 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

Next

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/ and email/<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
TODO
  • Remove the default hardcoded config and the encrypted keys from the code, there're just here for convenience.
  • Configure the Logger properly

About

A simple, minimal email API using third party APIs

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages