Skip to content

Commit

Permalink
Merge 57e1f0c into 37b597d
Browse files Browse the repository at this point in the history
  • Loading branch information
chris104957 committed Jun 15, 2019
2 parents 37b597d + 57e1f0c commit d834752
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 232 deletions.
2 changes: 1 addition & 1 deletion .coverage
@@ -1 +1 @@
!coverage.py: This is a private format, don't read it directly!{"lines":{"/Users/christopherdavies/maildown/maildown/__init__.py":[1,4,5],"/Users/christopherdavies/maildown/maildown/application.py":[1,2,4,6,7,8],"/Users/christopherdavies/maildown/maildown/commands.py":[1,2,5,14,16,39,45,47,61,72,74,17,18,19,20,21,23,24,26,27,29,30,32,33,35,36,48,49,51,52,55,56,75,76,78,79,80,81,83,87,94,95,96,97,98,99,101,102,104,105,84,85,88,89,90,92],"/Users/christopherdavies/maildown/maildown/utilities.py":[1,2,3,4,5,6,7,8,11,25,46,47,71,83,100,101,102,103,104,171,15,17,18,19,20,21,35,36,38,39,41,42,58,59,60,61,62,64,65,66,67,68,75,76,77,78,79,80,92,93,94,95,96,131,132,133,135,136,137,138,139,140,147,153,154,157,158,159,160,161,186,187,189,193,210,211,190,191,194,196,197,198,199,200,201,202,203,205],"/Users/christopherdavies/maildown/maildown/renderer.py":[1,2,3,4,5,6,7,8,11,14,16,17,28,18,19,20,21,22,41,42,44,45,47,48,50,52,53,54,55,56,57,60,61]}}
!coverage.py: This is a private format, don't read it directly!{"lines":{"/Users/christopherdavies/maildown/maildown/__init__.py":[1,4,5],"/Users/christopherdavies/maildown/maildown/application.py":[1,2,4,6,7,8],"/Users/christopherdavies/maildown/maildown/commands.py":[1,2,5,8,16,18,36,43,45,67,80,82,19,20,26,27,28,29,30,32,33,21,22,23,46,47,48,53,55,57,58,61,62,49,50,51,83,84,89,91,92,94,95,96,97,99,100,101,105,109,116,117,118,119,120,121,122,124,125,127,128,106,107,102,103,110,111,112,114,85,86,87],"/Users/christopherdavies/maildown/maildown/backends/__init__.py":[1],"/Users/christopherdavies/maildown/maildown/backends/aws.py":[1,2,3,4,5,6,9,10,14,15,16,17,18,54,68,69,80,100,102,103,73,74,75,76,77,90,91,94,95,97,98,114,115,116,117,118,120,121,122,123,124,23,24,25,27,28,29,30,31,32,39,45,46,49,50,51,56,57,58,59,60,61,62,64],"/Users/christopherdavies/maildown/maildown/backends/base.py":[1,2,5,6,9,14,20,28,29,30,32,35,38,42,43,54,55,33,7,15,16,10,11,12,17,18,21,22,23,24,25,57,58,60,64,70,71,61,62,65,67],"/Users/christopherdavies/maildown/maildown/utilities.py":[1,2,3,4,7,19,11,12,13,14,15,16,28,29,30,31,32],"/Users/christopherdavies/maildown/maildown/renderer.py":[1,2,3,4,5,6,7,8,11,14,16,17,26,18,19,20,21,22,39,40,42,43,45,46,48,50,51,52,53,54,55,58,59]}}
1 change: 1 addition & 0 deletions maildown/backends/__init__.py
@@ -0,0 +1 @@
from maildown.backends.aws import AwsBackend # noqa: F401
124 changes: 124 additions & 0 deletions maildown/backends/aws.py
@@ -0,0 +1,124 @@
from typing import Optional
import os
import configparser
from maildown.backends.base import BaseBackend
import boto3
from botocore.exceptions import ClientError


class AwsBackend(BaseBackend):
name = "aws"

def login( # type: ignore
self,
access_key: Optional[str] = None,
secret_key: Optional[str] = None,
region_name: str = "us-east-1",
aws_config_file: str = os.path.expanduser("~/.aws/credentials"),
) -> None:
"""
Retrieves, checks and stores AWS credentials to the maildown config file. Credentials are either
taken from the direct parameters, the environmental variables or the .aws/credentials file
"""
if not any([access_key, secret_key]):
access_key = os.environ.get("AWS_ACCESS_KEY_ID")
secret_key = os.environ.get("AWS_SECRET_ACCESS_KEY")

if not any([access_key, secret_key]):
config = configparser.ConfigParser()
config.read(aws_config_file)
try:
access_key = config["default"].get("aws_access_key_id")
secret_key = config["default"].get("aws_secret_access_key")

except KeyError:
raise KeyError(
f"Cannot find expected keys in config file stored at {aws_config_file}"
)

if not all([access_key, secret_key]):
raise AttributeError(
"No credentials supplied - you must either provide the `access_key`, and `secret_key` "
"values, set the environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, or run "
"`aws configure` and try again"
)
elif access_key and secret_key:
if not self.verify_auth(access_key, secret_key, region_name):
raise AttributeError("The supplied credentials are not valid")

self.config["access_key"] = access_key # type: ignore
self.config["secret_key"] = secret_key # type: ignore
self.config["region_name"] = region_name # type: ignore

def send_message(
self, to: list, sender: str, html: str, content: str, subject: str
):
return self.client.send_email(
Source=sender,
Destination=dict(ToAddresses=to),
Message=dict(
Body=dict(
Html=dict(Charset="utf-8", Data=html),
Text=dict(Charset="utf-8", Data=content),
),
Subject=dict(Charset="utf-8", Data=subject),
),
)

@property
def client(self) -> boto3.client:
"""
Returns an authenticated boto3.ses client
"""
return boto3.client(
"ses",
aws_access_key_id=self.config.get("access_key"), # type: ignore
aws_secret_access_key=self.config.get("secret_key"), # type: ignore
region_name=self.config.get("region", "us-east-1"), # type: ignore
)

def verify_address(self, email: str) -> bool:
"""
Asks Amazon to send an email to a given email address to verify the user's ownership of that address.
Email addresses must be verified by Amazon before you can send emails from them with SES
### Parameters:
- `email`: The email address to be verified
"""
addresses = self.client.list_verified_email_addresses().get(
"VerifiedEmailAddresses"
)

if email in addresses:
return True

self.client.verify_email_address(EmailAddress=email)
return False

@staticmethod
def verify_auth(
access_key: str, secret_key: str, region_name: str = "us-east-1"
) -> bool:
"""
Checks that the given credentials are valid by performing a simple call on the SES API
### Parameters:
- `access_key`: AWS access key
- `secret_key`: AWS secret key
- `region_name`: The AWS region name. Defaults to `us-east-1`
"""
client = boto3.client(
"ses",
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
region_name=region_name,
)
try:
client.list_configuration_sets()
return True
except ClientError:
return False
72 changes: 72 additions & 0 deletions maildown/backends/base.py
@@ -0,0 +1,72 @@
from typing import Optional, Any
from maildown import utilities, renderer


class BaseConfig(object):
def __init__(self, backend):
self.backend = backend

def __getitem__(self, item):
config = utilities.get_config()
backend_config = config.get(self.backend.name, {})
return backend_config[item]

def get(self, item, default: Optional[Any] = None):
try:
return self.__getitem__(item)
except KeyError:
return default

def __setitem__(self, key, value):
config = utilities.get_config()
backend_config = config.get(self.backend.name, {})
backend_config[key] = value
config[self.backend.name] = backend_config
utilities.write_config(**config)


class BaseBackend(object):
name = "base"
config = BaseConfig

def __init__(self):
self.config = BaseConfig(self)

def login(self, *args, **kwargs):
raise NotImplementedError()

def verify_address(self, email: str) -> bool:
raise NotImplementedError()

def send_message(
self, to: list, sender: str, html: str, content: str, subject: str
) -> None:
raise NotImplementedError()

def send(
self,
sender: str,
subject: str,
to: list,
content: Optional[str] = None,
file_path: Optional[str] = None,
context: Optional[dict] = None,
theme=None,
) -> None:

if not context:
context = {}

if file_path:
with open(file_path) as f:
content = f.read()

if content:
html = renderer.generate_content(content, context=context, theme=theme)

self.send_message(to, sender, html, content, subject)

else:
raise AttributeError(
"You must provide either the content or filepath attribute"
)
62 changes: 37 additions & 25 deletions maildown/commands.py
@@ -1,39 +1,35 @@
from cleo.commands import Command
from maildown import utilities
from maildown import backends


available_backends = dict(aws=backends.AwsBackend)


class InitCommand(Command):
"""
Configures Maildown for use
init
{access-key? : Your AWS Access Key ID}
{secret-key? : Your AWS Secret key}
{region? : AWS region to use (defaults to "us-east-1")}
{aws-config-file? : Path to your AWS config file (defaults to ~/.aws/credentials}
{--backend=aws : The email backend to use. Defaults to AWS SES }
{options?* : Arguments to pass to the backend's login methods, e.g. `access_key=1234`}
"""

def handle(self):
kwargs = dict()
access_key = self.argument("access-key")
secret_key = self.argument("secret-key")
region = self.argument("region")
aws_config_file = self.argument("aws-config-file")

if access_key:
kwargs["access_key"] = access_key

if secret_key:
kwargs["secret_key"] = secret_key

if region:
kwargs["region"] = region
__backend = available_backends.get(self.option("backend"))
if not __backend:
return self.line(
f'No backend called {self.option("backend")} exists', "error"
)

if aws_config_file:
kwargs["aws_config_file"] = aws_config_file
backend = __backend()
kwargs = dict()
for arg in self.argument("options"):
key, val = arg.split("=")
kwargs[key] = val

utilities.login(**kwargs)
self.info("Successfully set AWS credentials")
backend.login(**kwargs)
self.info("Initiated successfully")


class VerifyCommand(Command):
Expand All @@ -42,11 +38,19 @@ class VerifyCommand(Command):
verify
{email-address : The email address that you want to verify}
{--backend=aws : The email backend to use. Defaults to AWS SES }
"""

def handle(self):
email = self.argument("email-address")
verified = utilities.verify_address(email)
__backend = available_backends.get(self.option("backend"))
if not __backend:
return self.line(
f'No backend called {self.option("backend")} exists', "error"
)
backend = __backend()

verified = backend.verify_address(email)

if verified:
self.info("This email address has already been verified")
Expand All @@ -66,13 +70,21 @@ class SendCommand(Command):
{sender : The source email address (you must have verified ownership)}
{subject : The subject line of the email}
{--c|content=? : The content of the email to send}
{--backend=aws : The email backend to use. Defaults to AWS SES }
{--f|file-path=? : A path to a file containing content to send}
{--t|theme=? : A path to a css file to be applied to the email}
{--e|variable=* : Context variables to pass to the email, e.g. `-e name=Chris`}
{recipients?* : A list of email addresses to send the mail to}
"""

def handle(self):
__backend = available_backends.get(self.option("backend"))
if not __backend:
return self.line(
f'No backend called {self.option("backend")} exists', "error"
)
backend = __backend()

sender = self.argument("sender")
subject = self.argument("subject")

Expand Down Expand Up @@ -109,5 +121,5 @@ def handle(self):
if theme:
kwargs["theme"] = theme

utilities.send_message(**kwargs)
backend.send(**kwargs)
self.info("Messages added to queue")

0 comments on commit d834752

Please sign in to comment.