-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d29d3de
commit 27afcbb
Showing
19 changed files
with
573 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from django.contrib import admin | ||
|
||
# Register your models here. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class MailConfig(AppConfig): | ||
name = 'mail' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from django.contrib.auth.models import AbstractUser | ||
from django.db import models | ||
|
||
|
||
class User(AbstractUser): | ||
pass | ||
|
||
|
||
class Email(models.Model): | ||
user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="emails") | ||
sender = models.ForeignKey("User", on_delete=models.PROTECT, related_name="emails_sent") | ||
recipients = models.ManyToManyField("User", related_name="emails_received") | ||
subject = models.CharField(max_length=255) | ||
body = models.TextField(blank=True) | ||
timestamp = models.DateTimeField(auto_now_add=True) | ||
read = models.BooleanField(default=False) | ||
archived = models.BooleanField(default=False) | ||
|
||
def serialize(self): | ||
return { | ||
"id": self.id, | ||
"sender": self.sender.email, | ||
"recipients": [user.email for user in self.recipients.all()], | ||
"subject": self.subject, | ||
"body": self.body, | ||
"timestamp": self.timestamp.strftime("%b %d %Y, %I:%M %p"), | ||
"read": self.read, | ||
"archived": self.archived | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
document.addEventListener('DOMContentLoaded', function() { | ||
|
||
// Use buttons to toggle between views | ||
document.querySelector('#inbox').addEventListener('click', () => load_mailbox('inbox')); | ||
document.querySelector('#sent').addEventListener('click', () => load_mailbox('sent')); | ||
document.querySelector('#archived').addEventListener('click', () => load_mailbox('archive')); | ||
document.querySelector('#compose').addEventListener('click', compose_email); | ||
|
||
// By default, load the inbox | ||
load_mailbox('inbox'); | ||
}); | ||
|
||
function compose_email() { | ||
|
||
// Show compose view and hide other views | ||
document.querySelector('#emails-view').style.display = 'none'; | ||
document.querySelector('#compose-view').style.display = 'block'; | ||
|
||
// Clear out composition fields | ||
document.querySelector('#compose-recipients').value = ''; | ||
document.querySelector('#compose-subject').value = ''; | ||
document.querySelector('#compose-body').value = ''; | ||
} | ||
|
||
function load_mailbox(mailbox) { | ||
|
||
// Show the mailbox and hide other views | ||
document.querySelector('#emails-view').style.display = 'block'; | ||
document.querySelector('#compose-view').style.display = 'none'; | ||
|
||
// Show the mailbox name | ||
document.querySelector('#emails-view').innerHTML = `<h3>${mailbox.charAt(0).toUpperCase() + mailbox.slice(1)}</h3>`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
textarea { | ||
min-height: 400px; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
{% extends "mail/layout.html" %} | ||
{% load static %} | ||
|
||
{% block body %} | ||
<h2>{{ request.user.email }}</h2> | ||
|
||
<button class="btn btn-sm btn-outline-primary" id="inbox">Inbox</button> | ||
<button class="btn btn-sm btn-outline-primary" id="compose">Compose</button> | ||
<button class="btn btn-sm btn-outline-primary" id="sent">Sent</button> | ||
<button class="btn btn-sm btn-outline-primary" id="archived">Archived</button> | ||
<a class="btn btn-sm btn-outline-primary" href="{% url 'logout' %}">Log Out</a> | ||
<hr> | ||
|
||
<div id="emails-view"> | ||
</div> | ||
|
||
<div id="compose-view"> | ||
<h3>New Email</h3> | ||
<form id="compose-form"> | ||
<div class="form-group"> | ||
From: <input disabled class="form-control" value="{{ request.user.email }}"> | ||
</div> | ||
<div class="form-group"> | ||
To: <input id="compose-recipients" class="form-control"> | ||
</div> | ||
<div class="form-group"> | ||
<input class="form-control" id="compose-subject" placeholder="Subject"> | ||
</div> | ||
<textarea class="form-control" id="compose-body" placeholder="Body"></textarea> | ||
<input type="submit" class="btn btn-primary"/> | ||
</form> | ||
</div> | ||
{% endblock %} | ||
|
||
{% block script %} | ||
<script src="{% static 'mail/inbox.js' %}"></script> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{% load static %} | ||
|
||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<title>{% block title %}Mail{% endblock %}</title> | ||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> | ||
<link rel="stylesheet" href="{% static 'mail/styles.css' %}"> | ||
{% block script %} | ||
{% endblock %} | ||
</head> | ||
<body> | ||
<div class="container"> | ||
{% block body %} | ||
{% endblock %} | ||
</div> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{% extends "mail/layout.html" %} | ||
|
||
{% block body %} | ||
|
||
<h2>Login</h2> | ||
|
||
{% if message %} | ||
<div>{{ message }}</div> | ||
{% endif %} | ||
|
||
<form action="{% url 'login' %}" method="post"> | ||
{% csrf_token %} | ||
<div class="form-group"> | ||
<input autofocus class="form-control" type="email" name="email" placeholder="Email"> | ||
</div> | ||
<div class="form-group"> | ||
<input class="form-control" type="password" name="password" placeholder="Password"> | ||
</div> | ||
<input class="btn btn-primary" type="submit" value="Login"> | ||
</form> | ||
|
||
Don't have an account? <a href="{% url 'register' %}">Register here.</a> | ||
|
||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{% extends "mail/layout.html" %} | ||
|
||
{% block body %} | ||
|
||
<h2>Register</h2> | ||
|
||
{% if message %} | ||
<div>{{ message }}</div> | ||
{% endif %} | ||
|
||
<form action="{% url 'register' %}" method="post"> | ||
{% csrf_token %} | ||
<div class="form-group"> | ||
<input class="form-control" type="email" name="email" placeholder="Email Address"> | ||
</div> | ||
<div class="form-group"> | ||
<input class="form-control" type="password" name="password" placeholder="Password"> | ||
</div> | ||
<div class="form-group"> | ||
<input class="form-control" type="password" name="confirmation" placeholder="Confirm Password"> | ||
</div> | ||
<input class="btn btn-primary" type="submit" value="Register"> | ||
</form> | ||
|
||
Already have an account? <a href="{% url 'login' %}">Log In here.</a> | ||
|
||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from django.test import TestCase | ||
|
||
# Create your tests here. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from django.urls import path | ||
|
||
from . import views | ||
|
||
urlpatterns = [ | ||
path("", views.index, name="index"), | ||
path("login", views.login_view, name="login"), | ||
path("logout", views.logout_view, name="logout"), | ||
path("register", views.register, name="register"), | ||
|
||
# API Routes | ||
path("emails", views.compose, name="compose"), | ||
path("emails/<int:email_id>", views.email, name="email"), | ||
path("emails/<str:mailbox>", views.mailbox, name="mailbox"), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import json | ||
from django.contrib.auth import authenticate, login, logout | ||
from django.contrib.auth.decorators import login_required | ||
from django.db import IntegrityError | ||
from django.http import JsonResponse | ||
from django.shortcuts import HttpResponse, HttpResponseRedirect, render | ||
from django.urls import reverse | ||
from django.views.decorators.csrf import csrf_exempt | ||
|
||
from .models import User, Email | ||
|
||
|
||
def index(request): | ||
|
||
# Authenticated users view their inbox | ||
if request.user.is_authenticated: | ||
return render(request, "mail/inbox.html") | ||
|
||
# Everyone else is prompted to sign in | ||
else: | ||
return HttpResponseRedirect(reverse("login")) | ||
|
||
|
||
@csrf_exempt | ||
@login_required | ||
def compose(request): | ||
|
||
# Composing a new email must be via POST | ||
if request.method != "POST": | ||
return JsonResponse({"error": "POST request required."}, status=400) | ||
|
||
# Check recipient emails | ||
data = json.loads(request.body) | ||
emails = [email.strip() for email in data.get("recipients").split(",")] | ||
if emails == [""]: | ||
return JsonResponse({ | ||
"error": "At least one recipient required." | ||
}, status=400) | ||
|
||
# Convert email addresses to users | ||
recipients = [] | ||
for email in emails: | ||
try: | ||
user = User.objects.get(email=email) | ||
recipients.append(user) | ||
except User.DoesNotExist: | ||
return JsonResponse({ | ||
"error": f"User with email {email} does not exist." | ||
}, status=400) | ||
|
||
# Get contents of email | ||
subject = data.get("subject", "") | ||
body = data.get("body", "") | ||
|
||
# Create one email for each recipient, plus sender | ||
users = set() | ||
users.add(request.user) | ||
users.update(recipients) | ||
for user in users: | ||
email = Email( | ||
user=user, | ||
sender=request.user, | ||
subject=subject, | ||
body=body, | ||
read=user == request.user | ||
) | ||
email.save() | ||
for recipient in recipients: | ||
email.recipients.add(recipient) | ||
email.save() | ||
|
||
return JsonResponse({"message": "Email sent successfully."}, status=201) | ||
|
||
|
||
@login_required | ||
def mailbox(request, mailbox): | ||
|
||
# Filter emails returned based on mailbox | ||
if mailbox == "inbox": | ||
emails = Email.objects.filter( | ||
user=request.user, recipients=request.user, archived=False | ||
) | ||
elif mailbox == "sent": | ||
emails = Email.objects.filter( | ||
user=request.user, sender=request.user | ||
) | ||
elif mailbox == "archive": | ||
emails = Email.objects.filter( | ||
user=request.user, recipients=request.user, archived=True | ||
) | ||
else: | ||
return JsonResponse({"error": "Invalid mailbox."}, status=400) | ||
|
||
# Return emails in reverse chronologial order | ||
emails = emails.order_by("-timestamp").all() | ||
return JsonResponse([email.serialize() for email in emails], safe=False) | ||
|
||
|
||
@csrf_exempt | ||
@login_required | ||
def email(request, email_id): | ||
|
||
# Query for requested email | ||
try: | ||
email = Email.objects.get(user=request.user, pk=email_id) | ||
except Email.DoesNotExist: | ||
return JsonResponse({"error": "Email not found."}, status=404) | ||
|
||
# Return email contents | ||
if request.method == "GET": | ||
return JsonResponse(email.serialize()) | ||
|
||
# Update whether email is read or should be archived | ||
elif request.method == "PUT": | ||
data = json.loads(request.body) | ||
if data.get("read") is not None: | ||
email.read = data["read"] | ||
if data.get("archived") is not None: | ||
email.archived = data["archived"] | ||
email.save() | ||
return HttpResponse(status=204) | ||
|
||
# Email must be via GET or PUT | ||
else: | ||
return JsonResponse({ | ||
"error": "GET or PUT request required." | ||
}, status=400) | ||
|
||
|
||
def login_view(request): | ||
if request.method == "POST": | ||
|
||
# Attempt to sign user in | ||
email = request.POST["email"] | ||
password = request.POST["password"] | ||
user = authenticate(request, username=email, password=password) | ||
|
||
# Check if authentication successful | ||
if user is not None: | ||
login(request, user) | ||
return HttpResponseRedirect(reverse("index")) | ||
else: | ||
return render(request, "mail/login.html", { | ||
"message": "Invalid email and/or password." | ||
}) | ||
else: | ||
return render(request, "mail/login.html") | ||
|
||
|
||
def logout_view(request): | ||
logout(request) | ||
return HttpResponseRedirect(reverse("index")) | ||
|
||
|
||
def register(request): | ||
if request.method == "POST": | ||
email = request.POST["email"] | ||
|
||
# Ensure password matches confirmation | ||
password = request.POST["password"] | ||
confirmation = request.POST["confirmation"] | ||
if password != confirmation: | ||
return render(request, "mail/register.html", { | ||
"message": "Passwords must match." | ||
}) | ||
|
||
# Attempt to create new user | ||
try: | ||
user = User.objects.create_user(email, email, password) | ||
user.save() | ||
except IntegrityError as e: | ||
print(e) | ||
return render(request, "mail/register.html", { | ||
"message": "Email address already taken." | ||
}) | ||
login(request, user) | ||
return HttpResponseRedirect(reverse("index")) | ||
else: | ||
return render(request, "mail/register.html") |
Oops, something went wrong.