[email] Document functions, renamed recipients to subscribers

This commit is contained in:
maike 2020-07-06 20:51:07 +02:00 committed by dl6tom
parent 72bbbb1847
commit 8e1335bc06
3 changed files with 66 additions and 24 deletions

View file

@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: 0BSD
from kibicara.platforms.email.model import EmailRecipients, Email
from kibicara.platforms.email.model import EmailSubscribers, Email
from kibicara.model import Hood
from kibicara.platformapi import Censor, Spawner
from kibicara.email import send_email
@ -18,12 +18,13 @@ class EmailBot(Censor):
self.messages = []
async def run(self):
""" Loop which waits for new messages and sends emails to all subscribers. """
while True:
hood_name = await Hood.objects.get(id=self.model.hood).name
message = await self.receive()
for recipient in EmailRecipients(hood=self.model.hood):
for subscriber in EmailSubscribers(hood=self.model.hood):
json = {
'email': recipient.email,
'email': subscriber.email,
'hood': self.model.hood,
}
secretbox = SecretBox(Email.secret)
@ -40,7 +41,7 @@ class EmailBot(Censor):
"\n\n--\nIf you want to stop receiving these mails, "
"follow this link: " + unsubscribe_link
)
send_email(recipient.email, "Kibicara " + hood_name, body=message.text)
send_email(subscriber.email, "Kibicara " + hood_name, body=message.text)
spawner = Spawner(Email, EmailBot)

View file

@ -3,19 +3,21 @@
# SPDX-License-Identifier: 0BSD
from kibicara.model import Hood, Mapping
from ormantic import Integer, ForeignKey, Model, Text, DateTime
from ormantic import Integer, ForeignKey, Model, Text
class EmailRecipients(Model):
class EmailSubscribers(Model):
""" This table stores all subscribers, who want to receive messages via email. """
id: Integer(primary_key=True) = None
hood: ForeignKey(Hood)
email: Text()
class Mapping(Mapping):
table_name = 'email_recipients'
table_name = 'email_subscribers'
class Email(Model):
""" This table is used to track the hood ID. It also stores the token secret. """
id: Integer(primary_key=True) = None
hood: ForeignKey(Hood)
secret: Text()

View file

@ -4,7 +4,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from kibicara.platforms.email.bot import spawner
from kibicara.platforms.email.model import Email, EmailRecipients
from kibicara.platforms.email.model import Email, EmailSubscribers
from kibicara.platformapi import Message
from kibicara.config import config
from kibicara.email import send_email
@ -19,17 +19,24 @@ from os import urandom
class BodyMessage(BaseModel):
""" This model shows which values are supplied by the MDA listener script. """
text: str
to: str
author: str
secret: str
class Recipient(BaseModel):
class Subscriber(BaseModel):
""" This model holds the email address of a fresh subscriber. """
email: str
async def get_email_bot(to):
async def get_email_row(to: str):
""" Search for Email row if you only have an email address of a bot.
:param to: email address of a Kibicara hood, e.g. hood@kibicara.org
:return: row of Email table, belonging to that email address.
"""
hood_name = to.split('@')[0]
hood = await Hood.objects.get(name=hood_name)
try:
@ -44,6 +51,11 @@ mailbox_router = APIRouter()
@hood_router.post('/', status_code=status.HTTP_201_CREATED)
async def email_create(hood=Depends(get_hood)):
""" Create an Email bot. Call this when creating a hood.
:param hood: Hood.id of the hood the Email bot is supposed to belong to.
:return: Email row of the new email bot.
"""
try:
emailbot = await Email.objects.create(hood=hood, secret=urandom(32))
spawner.start(emailbot)
@ -54,23 +66,33 @@ async def email_create(hood=Depends(get_hood)):
@hood_router.delete('/', status_code=status.HTTP_200_OK)
async def email_delete(hood=Depends(get_hood)):
# who calls this function usually?
email_bot = await Email.objects.get(hood=hood)
""" Delete an Email bot. Call this when deleting a hood.
Stops and deletes the Email bot as well as all subscribers.
:param hood: Hood the Email bot belongs to.
"""
email_bot = await Email.objects.get(hood=hood.id)
spawner.stop(email_bot)
await EmailRecipients.objects.delete_many(hood=hood)
await EmailSubscribers.objects.delete_many(hood=hood.id)
await email_bot.delete()
@hood_router.post('/recipient/')
async def email_recipient_create(recipient: Recipient, hood=Depends(get_hood)):
@hood_router.post('/subscribe/')
async def email_subscribe(subscriber: Subscriber, hood=Depends(get_hood)):
""" Send a confirmation mail to subscribe to messages via email.
:param subscriber: Subscriber object, holds the email address.
:param hood: Hood the Email bot belongs to.
:return: Returns status code 200 after sending confirmation email.
"""
secretbox = SecretBox(Email.secret)
token = secretbox.encrypt({'email': recipient.email,}, encoder=URLSafeBase64Encoder)
token = secretbox.encrypt({'email': subscriber.email, }, encoder=URLSafeBase64Encoder)
asciitoken = token.decode('ascii')
confirm_link = (
config['root_url'] + "api/" + hood.id + "/email/recipient/confirm/" + asciitoken
config['root_url'] + "api/" + hood.id + "/email/subscribe/confirm/" + asciitoken
)
send_email(
recipient.email,
subscriber.email,
"Subscribe to Kibicara " + hood.name,
sender=hood.name,
body="To confirm your subscription, follow this link: " + confirm_link,
@ -78,30 +100,47 @@ async def email_recipient_create(recipient: Recipient, hood=Depends(get_hood)):
return status.HTTP_200_OK
@hood_router.post('/recipient/confirm/{token}')
async def email_recipient_confirm(token, hood=Depends(get_hood)):
@hood_router.post('/subscribe/confirm/{token}')
async def email_subscribe_confirm(token, hood=Depends(get_hood)):
""" Confirm a new subscriber and add them to the database.
:param token: encrypted JSON token, holds the email of the subscriber.
:param hood: Hood the Email bot belongs to.
:return: Returns status code 200 after adding the subscriber to the database.
"""
secretbox = SecretBox(Email.secret)
json = secretbox.decrypt(token.encode('ascii'), encoder=URLSafeBase64Encoder)
try:
await EmailRecipients.objects.create(hood=hood.id, email=json['email'])
await EmailSubscribers.objects.create(hood=hood.id, email=json['email'])
return status.HTTP_201_CREATED
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@hood_router.get('/unsubscribe/{token}', status_code=status.HTTP_200_OK)
async def email_recipient_unsubscribe(token, hood=Depends(get_hood)):
async def email_unsubscribe(token, hood=Depends(get_hood)):
""" Remove a subscriber from the database when they click on an unsubscribe link.
:param token: encrypted JSON token, holds subscriber email + hood.id.
:param hood: Hood the Email bot belongs to.
"""
secretbox = SecretBox(Email.secret)
json = secretbox.decrypt(token.encode('ascii'), encoder=URLSafeBase64Encoder)
# If token.hood and url.hood are different, raise an error:
if hood.id is not json['hood']:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
await EmailRecipients.objects.delete_many(hood=json['hood'], email=json['email'])
await EmailSubscribers.objects.delete_many(hood=json['hood'], email=json['email'])
@mailbox_router.post('/messages/')
async def email_message_create(message: BodyMessage):
""" Receive a message from the MDA and pass it to the censor.
:param message: BodyMessage object, holds the message.
:return: returns status code 201 if the message is accepted by the censor.
"""
# get bot via "To:" header
email_bot = await get_email_bot(message.to)
email_bot = await get_email_row(message.to)
# check API secret
if message.secret is not email_bot.secret:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)