diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..03d05b5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +venv +.vscode \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 48987b8..50b96b3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,9 +5,13 @@ name: Python package on: push: - branches: master + branches: + - dev + - master pull_request: - branches: master + branches: + - dev + - master jobs: build: @@ -16,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10"] env: DEBUG: true @@ -41,7 +45,7 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | - cd dry + cd src mkdir logs pytest --cov-report xml - name: Upload Coverage to Codecov diff --git a/.gitignore b/.gitignore index b6e4761..f3b3cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ coverage.xml local_settings.py db.sqlite3 db.sqlite3-journal +media/ # Flask stuff: instance/ @@ -127,3 +128,5 @@ dmypy.json # Pyre type checker .pyre/ + +*.csv \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index d81e1af..be6581f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,8 @@ { "autoDocstring.docstringFormat": "sphinx", - "editor.tabSize": 4 + "editor.tabSize": 4, + "cSpell.words": [ + "sents", + "Zepto" + ] } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..797fc7a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11 + +ENV PYTHONUNBUFFERED 1 + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN chmod +x ./src/entrypoint.sh + +EXPOSE 8000 diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..721dc7d --- /dev/null +++ b/compose.yml @@ -0,0 +1,16 @@ +version: '3.5' +services: + thunder: + build: . + ports: + - "7000:8000" + container_name: thunder + volumes: + - db-data:/app/src + env_file: + - ./src/.env + working_dir: /app/src + entrypoint: ./entrypoint.sh + +volumes: + db-data: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2d9cbc7..7c66be8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,32 +1,42 @@ -asgiref==3.5.2 -atomicwrites==1.4.0 -attrs==21.4.0 -certifi==2022.6.15 -charset-normalizer==2.1.0 -colorama==0.4.5 -coverage==6.4.1 -Django==4.0.5 -execnet==1.9.0 -idna==3.3 -iniconfig==1.1.1 -numpy==1.23.0 -packaging==21.3 -pandas==1.4.3 -pluggy==1.0.0 -py==1.11.0 -pyparsing==3.0.9 -pytest==7.1.2 -pytest-cov==3.0.0 -pytest-django==4.5.2 -pytest-forked==1.4.0 -pytest-lazy-fixture==0.6.3 -pytest-xdist==2.5.0 -python-dateutil==2.8.2 -python-decouple==3.6 -pytz==2022.1 -requests==2.28.1 -six==1.16.0 -sqlparse==0.4.2 -tomli==2.0.1 -tzdata==2022.1 -urllib3==1.26.9 +aiohappyeyeballs==2.3.5 +aiohttp==3.10.3 +aiohttp-retry==2.8.3 +aiosignal==1.3.1 +asgiref==3.5.2 +atomicwrites==1.4.0 +attrs==21.4.0 +certifi==2022.6.15 +charset-normalizer==2.1.0 +colorama==0.4.5 +coverage==6.4.1 +Django==4.0.5 +django-quill-editor==0.1.40 +execnet==1.9.0 +frozenlist==1.4.1 +idna==3.3 +iniconfig==1.1.1 +multidict==6.0.5 +numpy==1.23.0 +packaging==21.3 +pandas==1.4.3 +pluggy==1.0.0 +py==1.11.0 +PyJWT==2.9.0 +pyparsing==3.0.9 +pytest==7.1.2 +pytest-cov==3.0.0 +pytest-django==4.5.2 +pytest-forked==1.4.0 +pytest-lazy-fixture==0.6.3 +pytest-xdist==2.5.0 +python-dateutil==2.8.2 +python-decouple==3.6 +pytz==2022.1 +requests==2.28.1 +six==1.16.0 +sqlparse==0.4.2 +tomli==2.0.1 +twilio==9.2.3 +tzdata==2022.1 +urllib3==1.26.9 +yarl==1.9.4 diff --git a/src/.env.dev b/src/.env.dev new file mode 100644 index 0000000..8358473 --- /dev/null +++ b/src/.env.dev @@ -0,0 +1,5 @@ +SECRET_KEY='test' +DEBUG=True +BLOCK_EMAIL=False +DEBUG_EMAIL=False +DJANGO_SETTINGS_MODULE='config.settings.dev' diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 41e45a0..c225b86 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -2,7 +2,7 @@ body { background-color: var(--bs-dark); } -.navbar{ +.navbar { padding-top: 1.5rem; padding-bottom: 1.5rem; background-color: #131518 !important; @@ -13,30 +13,30 @@ body { gap: 1rem; } -.container-fluid{ +.container-fluid { max-width: 900px; margin: 0 auto; } -label{ +label { margin-bottom: 6px; font-weight: 500; } -form{ - max-width: 500px; +.box-container { + max-width: 900px; margin: auto; background: white; padding: 2rem; border-radius: 10px; } -legend{ +legend { margin: 1rem 0 2rem 0; font-weight: bold; } -form button.btn { +#sender button.btn { margin-top: 3rem !important; display: block; padding: 1.1rem 2rem; @@ -45,3 +45,36 @@ form button.btn { font-size: 1.1rem; } + +.manager-form { + display: flex; + gap: 12px; + flex-wrap: wrap; + background: #212529; + color: white; + padding: 1rem; + border-radius: 12px; +} + +.manager-form p { + flex: 1; + min-width: 200px; +} + + +#manager-details { + display: flex; + gap: 12px; + flex-wrap: wrap; +} +#manager-details .manager-detail-item { + flex: 1; + min-width: 200px; + display: flex; + gap: 12px; + flex-wrap: wrap; +} +#manager-details .manager-detail-item strong { + font-weight: 500; + text-transform: capitalize; +} diff --git a/src/assets/js/main.js b/src/assets/js/main.js new file mode 100644 index 0000000..62b1f16 --- /dev/null +++ b/src/assets/js/main.js @@ -0,0 +1,49 @@ +const fileInput = document.getElementById("csv"); + +const previewCSVData = async (dataurl) => { + const d = await d3.csv(dataurl); + console.log({ + d, + }); + console.log(d.columns); + + const email_key = document.getElementById("email_key"); + // delete previous options + email_key.options.length = 0; + + d.columns.map((col) => { + email_key.options[email_key.options.length] = new Option(col, col); + }); + + $(function () { + $("#slider-range").slider({ + range: true, + min: 0, + max: d.length, + values: [0, d.length], + slide: function (event, ui) { + $("#start").val(ui.values[0]); + $("#stop").val(ui.values[1]); + }, + }); + $("#start").val($("#slider-range").slider("values", 0)); + $("#stop").val($("#slider-range").slider("values", 1)); + }); +}; + +const readFile = (e) => { + const file = fileInput.files[0]; + if (!file) { + return; + } + const reader = new FileReader(); + reader.onload = () => { + const dataUrl = reader.result; + previewCSVData(dataUrl); + }; + reader.readAsDataURL(file); +}; + +if (fileInput) { + fileInput.onchange = readFile; +} diff --git a/src/config/settings/base.py b/src/config/settings/base.py index 2b6fd1d..1b90b9e 100644 --- a/src/config/settings/base.py +++ b/src/config/settings/base.py @@ -4,7 +4,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent.parent -SECRET_KEY = config('SECRET_KEY', default='test') +SECRET_KEY = config('SECRET_KEY', default='django-insecure-ddd1') DEBUG = config('DEBUG', default=True, cast=bool) @@ -19,7 +19,8 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', - 'mailer' + 'mailer', + 'django_quill', ] MIDDLEWARE = [ @@ -124,14 +125,14 @@ 'handlers': { 'basic_h': { 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - # 'filename': BASE_DIR / 'logs/debug.log', + 'class': 'logging.FileHandler', + 'filename': BASE_DIR / 'logs/debug.log', 'formatter': 'simple', }, 'basic_e': { 'level': 'WARNING', - 'class': 'logging.StreamHandler', - # 'filename': BASE_DIR / 'logs/error.log', + 'class': 'logging.FileHandler', + 'filename': BASE_DIR / 'logs/error.log', 'formatter': 'simple', }, }, @@ -149,3 +150,27 @@ TWILIO_SID = config('TWILIO_SID', default='test') TWILIO_TOKEN = config('TWILIO_TOKEN', default='test') + + +QUILL_CONFIGS = { + 'default':{ + 'theme': 'snow', + 'modules': { + 'syntax': True, + 'toolbar': [ + [ + {'font': []}, + {'header': [1, 2, 3, 4, 5, 6, False]}, + {'align': []}, + 'bold', 'italic', 'underline', 'strike', 'blockquote', + {'color': []}, + {'background': []}, + ], + [{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }], + ['code-block', 'link'], + ['clean'], + ] + } + } +} + diff --git a/src/config/settings/dev.py b/src/config/settings/dev.py index 2b07311..ce292eb 100644 --- a/src/config/settings/dev.py +++ b/src/config/settings/dev.py @@ -4,8 +4,5 @@ ALLOWED_HOSTS = ["*"] -SEND_GRID = config('SEND_GRID', default='test') -ZEPTOTOKEN = config('ZEPTOTOKEN', default='test') -EMAIL_DOMAIN = config('EMAIL_DOMAIN', default='test') -BLOCK_EMAIL = config('BLOCK_EMAIL', default=True, cast=bool) -DEBUG_EMAIL = config('DEBUG_EMAIL', default=True, cast=bool) +BLOCK_EMAIL = config('BLOCK_EMAIL', default=False, cast=bool) +DEBUG_EMAIL = config('DEBUG_EMAIL', default=False, cast=bool) diff --git a/src/entrypoint.sh b/src/entrypoint.sh new file mode 100644 index 0000000..a62159e --- /dev/null +++ b/src/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +python manage.py migrate +python manage.py collectstatic --no-input +python manage.py runserver 0.0.0.0:8000 diff --git a/src/file.csv b/src/file.csv deleted file mode 100644 index b114d91..0000000 --- a/src/file.csv +++ /dev/null @@ -1,5 +0,0 @@ -first_name,last_name,email -ayo,israel,sketcherslodge@gmail.com -jacob,john,bossdollyp@gmail.com -rita,junior,netrobepy@gmail.com -damian,boy,netrobepy1@gmail.com diff --git a/src/mailer/forms.py b/src/mailer/forms.py index 2ca780f..85ada87 100644 --- a/src/mailer/forms.py +++ b/src/mailer/forms.py @@ -1,12 +1,15 @@ from django import forms from django.forms import ValidationError from django.core.validators import FileExtensionValidator, MinValueValidator +from django_quill.forms import QuillFormField -from .models import Uploads +from mailer.mail_manager import MAIL_MANAGERS + +from .models import EmailManager, Uploads validator_file_ext = FileExtensionValidator( - allowed_extensions=['xls', 'gz', 'csv', 'xlsx'] + allowed_extensions=["xls", "gz", "csv", "xlsx"] ) validator_start_min = MinValueValidator(limit_value=1) validator_stop_min = MinValueValidator(limit_value=1) @@ -17,27 +20,57 @@ class MailForm(forms.Form): sender = forms.CharField() reply_to = forms.EmailField(required=False) subject = forms.CharField() - message = forms.CharField(widget=forms.Textarea) + content = QuillFormField() + email_key = forms.CharField() start = forms.IntegerField(validators=[validator_start_min]) stop = forms.IntegerField(validators=[validator_stop_min]) + attachments = forms.FileField( + widget=forms.ClearableFileInput(attrs={"multiple": True}), required=False + ) + mail_manager = forms.ModelChoiceField( + queryset=EmailManager.objects.all(), + empty_label=None, + widget=forms.Select(attrs={"class": "form-select"}), + required=True, + ) def clean(self): cleaned = super().clean() # Validate stop is greater than start - start = cleaned.get('start') - stop = cleaned.get('stop') + start = cleaned.get("start") + stop = cleaned.get("stop") if (start is not None and stop is not None) and (start > stop): error = ValidationError( - 'Start must not be greater that stop', - code='start_stop_error') - self.add_error('start', error) + "Start must not be greater that stop", code="start_stop_error" + ) + self.add_error("start", error) return cleaned def save(self, commit=True): - file = self.cleaned_data.get('file') + file = self.cleaned_data.get("file") obj = Uploads(file=file) if commit: obj.save() return obj + + +class EmailManagerConfigForm(forms.ModelForm): + label = forms.CharField( + widget=forms.TextInput(attrs={"class": "form-control"}), + help_text="Enter a label for this email manager", + ) + mail_manager = forms.ChoiceField( + choices=MAIL_MANAGERS, + widget=forms.Select(attrs={"class": "form-select"}), + help_text="Select the email manager", + ) + + class Meta: + model = EmailManager + fields = ["label", "mail_manager"] + + def save(self, config, *args, **kwargs): + self.instance.config = config + return super().save(*args, **kwargs) diff --git a/src/mailer/mail_manager.py b/src/mailer/mail_manager.py new file mode 100644 index 0000000..49a15c9 --- /dev/null +++ b/src/mailer/mail_manager.py @@ -0,0 +1,64 @@ +from django import forms +import socket + +from messenger.email_manager import SendGridEmailManager, SmtpEmailManager, ZeptoEmailManager + + +class ManagerForm(forms.Form): + manager = None + + def __init__(self, manager: str = None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.manager = manager + + +class SMTPForm(ManagerForm): + host = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"})) + port = forms.IntegerField(widget=forms.NumberInput(attrs={"class": "form-control"})) + username = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"})) + password = forms.CharField( + widget=forms.PasswordInput(attrs={"class": "form-control"}) + ) + + def clean_host(self): + host = self.cleaned_data.get("host") + try: + # Validate the host by resolving it + socket.gethostbyname(host) + except socket.error: + raise forms.ValidationError( + "Invalid host. Please enter a valid hostname or IP address." + ) + return host + + def clean_port(self): + port = self.cleaned_data.get("port") + if port < 1 or port > 65535: + raise forms.ValidationError("Port must be between 1 and 65535.") + return port + + +class ApiKeyForm(ManagerForm): + api_key = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"})) + + +MAIL_MANAGERS = ( + ("sendgrid", "Sendgrid"), + ("zepto", "ZeptoMail"), + ("smtp", "SMTP Server"), +) + +MANAGER_CONFIG = { + "sendgrid": { + "form": ApiKeyForm, + "manager": SendGridEmailManager, + }, + "zepto": { + "form": ApiKeyForm, + "manager": ZeptoEmailManager, + }, + "smtp": { + "form": SMTPForm, + "manager": SmtpEmailManager + }, +} diff --git a/src/mailer/migrations/0002_emailmanager.py b/src/mailer/migrations/0002_emailmanager.py new file mode 100644 index 0000000..a2b6b5e --- /dev/null +++ b/src/mailer/migrations/0002_emailmanager.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.5 on 2025-01-04 20:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailer', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='EmailManager', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.CharField(max_length=255)), + ('mail_manager', models.CharField(choices=[('sendgrid', 'SendgridEmailManager'), ('zepto', 'ZeptoEmailManager'), ('smtp', 'SMTPEmailManager')], max_length=10)), + ('config', models.JSONField()), + ], + ), + ] diff --git a/src/mailer/migrations/0003_alter_emailmanager_mail_manager.py b/src/mailer/migrations/0003_alter_emailmanager_mail_manager.py new file mode 100644 index 0000000..f000872 --- /dev/null +++ b/src/mailer/migrations/0003_alter_emailmanager_mail_manager.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.5 on 2025-01-04 20:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailer', '0002_emailmanager'), + ] + + operations = [ + migrations.AlterField( + model_name='emailmanager', + name='mail_manager', + field=models.CharField(choices=[('sendgrid', 'Sendgrid'), ('zepto', 'ZeptoMail'), ('smtp', 'SMTP Server')], max_length=10), + ), + ] diff --git a/src/mailer/models.py b/src/mailer/models.py index 17dd0a1..21cc064 100644 --- a/src/mailer/models.py +++ b/src/mailer/models.py @@ -1,5 +1,10 @@ +import json from django.db import models +from mailer.mail_manager import MAIL_MANAGERS, MANAGER_CONFIG +from messenger.email_manager import BaseEmailManager +from django.conf import settings + class Uploads(models.Model): """ @@ -10,7 +15,39 @@ class Uploads(models.Model): :return: Uploads :rtype: models.Model """ + file = models.FileField() def __str__(self): return self.file.name + + +class EmailManager(models.Model): + label = models.CharField(max_length=255) + mail_manager = models.CharField(max_length=10, choices=MAIL_MANAGERS) + config = models.JSONField() + + @property + def config_json(self): + return json.dumps( + { + "label": self.label, + "mail_manager": self.get_mail_manager_display(), + **self.config, + } + ) + + def get_email_manager(self, sender: str, reply_to: str) -> BaseEmailManager: + email_manager: BaseEmailManager = MANAGER_CONFIG.get(self.mail_manager).get( + "manager" + ) + return email_manager( + **self.config, + sender=f"{sender}@{settings.EMAIL_DOMAIN}", + block_send=settings.BLOCK_EMAIL, + debug=settings.DEBUG_EMAIL, + reply_email=reply_to, + ) + + def __str__(self): + return f"{self.label} - {self.get_mail_manager_display()}" diff --git a/src/mailer/templates/mailer/base.html b/src/mailer/templates/mailer/base.html index f562fe8..ae4343e 100644 --- a/src/mailer/templates/mailer/base.html +++ b/src/mailer/templates/mailer/base.html @@ -16,7 +16,7 @@ {% endif %} - + @@ -49,6 +49,13 @@ btn btn-outline-primary btn-sm" aria-current="page" href="{% url 'mailer:phone' %}"> Send SMS + + Settings + @@ -65,18 +72,18 @@ {% block content %} - {% endblock content %} - + + + {% block extra_js %} - {% endblock extra_js %} diff --git a/src/mailer/templates/mailer/email_templates/email.html b/src/mailer/templates/mailer/email_templates/email.html index 231df7d..1ca74d3 100644 --- a/src/mailer/templates/mailer/email_templates/email.html +++ b/src/mailer/templates/mailer/email_templates/email.html @@ -85,14 +85,6 @@

- - {% block footer %} - - {% endblock footer %} - - diff --git a/src/mailer/templates/mailer/email_templates/template1.html b/src/mailer/templates/mailer/email_templates/template1.html index 8d5733e..4ebb1eb 100644 --- a/src/mailer/templates/mailer/email_templates/template1.html +++ b/src/mailer/templates/mailer/email_templates/template1.html @@ -5,13 +5,3 @@ {{ message|safe }} {% endblock contents %} - - -{% block footer %} - -

Best regards,

-

Ayanwola Ayomide

-

GDSC Lead

-

TechFest 24 Organizing Team

- -{% endblock footer %} \ No newline at end of file diff --git a/src/mailer/templates/mailer/index.html b/src/mailer/templates/mailer/index.html index 0e56e5a..16885b5 100644 --- a/src/mailer/templates/mailer/index.html +++ b/src/mailer/templates/mailer/index.html @@ -1,92 +1,122 @@ {% extends 'mailer/base.html' %} {% load mytags %} - {% block content %} -
-
+ {% csrf_token %} + {{ form.media }} Send Email -
- - - - {% for error in form.sender.errors %} - {{ error }} - {% endfor %} - -
- -
- - - - {% for error in form.reply_to.errors %} - {{ error }} - {% endfor %} - -
- -
- - - - {% for error in form.subject.errors %} - {{ error }} - {% endfor %} - -
- -
- - - - {% for error in form.message.errors %} - {{ error }} - {% endfor %} - -
- -
-
- - - - {% for error in form.start.errors %} +
+
+
+
+ + + {% for error in form.sender.errors %} + {{ error }} + {% endfor %} +
+
+ + + {% for error in form.reply_to.errors %} + {{ error }} + {% endfor %} +
+
+ +
+ + + {% for error in form.subject.errors %} {{ error }} - {% endfor %} - + {% endfor %} +
+ +
+ + + + {% for error in form.file.errors %} + {{ error }} + {% endfor %} +
+ +
+ + + + {% for error in form.attachments.errors %} + {{ error }} + {% endfor %} +
-
- - - - {% for error in form.stop.errors %} +
+
+
+
+ + + {% for error in form.start.errors %} + {{ error }} + {% endfor %} +
+ - +
+ + + {% for error in form.stop.errors %} + {{ error }} + {% endfor %} +
+
+
+
+ +
+ + + {% for error in form.email_key.errors %} + {{ error }} + {% endfor %} +
+ +
+ + {{ form.mail_manager }} + {% for error in form.mail_manager.errors %} {{ error }} - {% endfor %} - + {% endfor %} +
- +
- - - - {% for error in form.file.errors %} - {{ error }} + + {{ form.content }} + {% for error in form.content.errors %} + {{ error }} {% endfor %} -
- + - +
{% endblock content %} - \ No newline at end of file + + +{% block extra_js %} + +{% endblock extra_js %} \ No newline at end of file diff --git a/src/mailer/templates/mailer/settings.html b/src/mailer/templates/mailer/settings.html new file mode 100644 index 0000000..4e5d40b --- /dev/null +++ b/src/mailer/templates/mailer/settings.html @@ -0,0 +1,168 @@ +{% extends 'mailer/base.html' %} +{% load mytags %} + +{% block content %} + +
+
+ Create Email Manager + +
+ {% csrf_token %} +
+
+
+
+ + {{ form.label }} + {% for error in form.label.errors %} + {{ error }} + {% endfor %} +
+
+ + {{ form.mail_manager }} + {% for error in form.mail_manager.errors %} + {{ error }} + {% endfor %} +
+
+
+
+
+ {% for mform in forms %} + + {% endfor %} +
+
+
+ + +
+ +
+

Managers

+ + + + + + + + + + + + {% for email_manager in email_managers %} + + + + + + + {% endfor %} + + +
#LabelManagerActions
1{{ email_manager.label }}{{ email_manager.get_mail_manager_display }} +
+ + +
+
+
+
+ + {{ managers|json_script:"managers_list" }} + + + + +
+ +{% endblock content %} + + +{% block extra_js %} + + + +{% endblock extra_js %} + \ No newline at end of file diff --git a/src/mailer/templates/mailer/sms.html b/src/mailer/templates/mailer/sms.html index 18a46e2..3b2d846 100644 --- a/src/mailer/templates/mailer/sms.html +++ b/src/mailer/templates/mailer/sms.html @@ -6,7 +6,7 @@
-
+ Send SMS diff --git a/src/mailer/urls.py b/src/mailer/urls.py index 1fddea7..f83b626 100644 --- a/src/mailer/urls.py +++ b/src/mailer/urls.py @@ -4,5 +4,6 @@ app_name = 'mailer' urlpatterns = [ path('', views.Dashboard.as_view(), name='home'), - path('send-sms/', views.SmsDashboard.as_view(), name='phone') + path('send-sms/', views.SmsDashboard.as_view(), name='phone'), + path('settings/', views.Settings.as_view(), name='settings'), ] diff --git a/src/mailer/views.py b/src/mailer/views.py index 0979540..6fc1ef5 100644 --- a/src/mailer/views.py +++ b/src/mailer/views.py @@ -1,31 +1,40 @@ +import json import os +from typing import Any from django.conf import settings from django.contrib import messages from django.shortcuts import redirect -from django.views.generic import TemplateView -from messenger.email_manager import ZeptoEmailManager +from django.views.generic import TemplateView, ListView +from mailer.mail_manager import MANAGER_CONFIG +from mailer.models import EmailManager from messenger.sms_manager import SmsManager from messenger.messager import ExcelMessenger from messenger.messsage_manager import HtmlMessageManager from utils.general import count_true_in_iter -from utils.loggers import err_logger, logger # noqa +from utils.loggers import err_logger # noqa -from .forms import MailForm +from .forms import EmailManagerConfigForm, MailForm class Dashboard(TemplateView): """ Email Sender Dashboard """ - template_name = 'mailer/index.html' - message_template = 'mailer/email_templates/template1.html' + + template_name = "mailer/index.html" + message_template = "mailer/email_templates/template1.html" extra_context = { - 'title': 'Email Sender', - 'page': 'home', + "title": "Email Sender", + "page": "home", } form_class = MailForm + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + context = super().get_context_data(**kwargs) + context["form"] = MailForm() + return context + def get_message_context(self, data: dict): """ Get message context @@ -35,9 +44,7 @@ def get_message_context(self, data: dict): :return: message context :rtype: dict """ - return { - 'reply_to': data.get('reply_to') - } + return {"reply_to": data.get("reply_to")} def create_message_manager(self, data: dict): """ @@ -51,7 +58,7 @@ def create_message_manager(self, data: dict): return HtmlMessageManager( template_name=self.message_template, request=self.request, - context=self.get_message_context(data) + context=self.get_message_context(data), ) def create_sender_manager(self, data: dict): @@ -63,15 +70,10 @@ def create_sender_manager(self, data: dict): :return: sender manager :rtype: Email manager """ - sender = data.get('sender') - reply_to = data.get('reply_to') - return ZeptoEmailManager( - api_key=settings.ZEPTOTOKEN, - sender=f"{sender}@{settings.EMAIL_DOMAIN}", - block_send=settings.BLOCK_EMAIL, - debug=settings.DEBUG_EMAIL, - reply_email=reply_to - ) + sender = data.get("sender") + reply_to = data.get("reply_to") + mail_manager = data.get("mail_manager") + return mail_manager.get_email_manager(sender, reply_to) def create_messenger(self, file_path: str, data: dict): """ @@ -84,19 +86,16 @@ def create_messenger(self, file_path: str, data: dict): :return: messenger :rtype: ExcelMessenger """ - start = data.get('start') - stop = data.get('stop') + start = data.get("start") + stop = data.get("stop") messenger = ExcelMessenger( start=start, stop=stop, - file_path=file_path - ) - messenger.set_sender_manager( - self.create_sender_manager(data) - ) - messenger.set_message_manager( - self.create_message_manager(data) + file_path=file_path, + recipient_field=data.get("email_key"), ) + messenger.set_sender_manager(self.create_sender_manager(data)) + messenger.set_message_manager(self.create_message_manager(data)) return messenger def post(self, request, **kwargs): @@ -108,31 +107,39 @@ def post(self, request, **kwargs): if form.is_valid(): obj = form.save() - subject = form.cleaned_data.get('subject') - message = form.cleaned_data.get('message') - start = form.cleaned_data.get('start') - stop = form.cleaned_data.get('stop') + subject = form.cleaned_data.get("subject") + message = json.loads(form.cleaned_data.get("content")).get("html") + start = form.cleaned_data.get("start") + stop = form.cleaned_data.get("stop") + + attachments = [] + files = request.FILES.getlist("attachments") + for file in files: + attachments.append( + { + "filename": file.name, + "data": file.read(), + "mime_type": file.content_type, + } + ) file_path = obj.file.path messages_to_be_sent = stop - start + 1 completed = False - + try: messenger = self.create_messenger(file_path, form.cleaned_data) - message = message.replace('\n', '
') - print(message) - sents_fails = messenger.start_process( - subject=subject, - message=message + subject=subject, message=message, attachments=attachments ) sents = count_true_in_iter(sents_fails) fails = messages_to_be_sent - sents - response_message = "{} messages sent, {} messages failed"\ - .format(sents, fails) + response_message = "{} messages sent, {} messages failed".format( + sents, fails + ) messages.success(request, response_message) completed = True @@ -153,16 +160,17 @@ class SmsDashboard(Dashboard): """ SMS Sender Dashboard """ - template_name = 'mailer/sms.html' - message_template = 'mailer/sms_templates/template1.html' + + template_name = "mailer/sms.html" + message_template = "mailer/sms_templates/template1.html" extra_context = { - 'title': 'SMS Sender', - 'page': 'phone', + "title": "SMS Sender", + "page": "phone", } def get_message_context(self, data: dict): return { - 'subject': data.get('subject'), + "subject": data.get("subject"), } def create_sender_manager(self, data: dict): @@ -174,7 +182,7 @@ def create_sender_manager(self, data: dict): :return: sender manager :rtype: SmsManager """ - sender = data.get('sender') + sender = data.get("sender") return SmsManager( sid=settings.TWILIO_SID, token=settings.TWILIO_TOKEN, @@ -182,3 +190,51 @@ def create_sender_manager(self, data: dict): block_send=False and settings.BLOCK_EMAIL, debug=settings.DEBUG_EMAIL, ) + + +class Settings(ListView): + """ + Email Sender Settings + """ + + template_name = "mailer/settings.html" + extra_context = { + "title": "Settings", + "page": "settings", + } + model = EmailManager + context_object_name = "email_managers" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form"] = EmailManagerConfigForm() + forms_dict = { + manager: MANAGER_CONFIG[manager]["form"](manager=manager, prefix=manager) + for manager in MANAGER_CONFIG.keys() + } + context["forms"] = list(forms_dict.values()) + context["managers"] = {"managers": list(MANAGER_CONFIG.keys())} + return context + + def post(self, request, **kwargs): + self.object_list = self.get_queryset() + context = self.get_context_data() + form = EmailManagerConfigForm(data=request.POST) + if form.is_valid(): + manager = form.cleaned_data.get("mail_manager") + manager_form = MANAGER_CONFIG[manager]["form"](data=request.POST, prefix=manager, manager=manager) + if manager_form.is_valid(): + config = manager_form.cleaned_data + form.save(config) + messages.success(request, "Manager added successfully") + return redirect("mailer:settings") + else: + messages.error(request, "Invalid form data") + forms_dict = { + manager: MANAGER_CONFIG[manager]["form"](manager=manager, prefix=manager) + for manager in MANAGER_CONFIG.keys() + } + forms_dict[manager] = manager_form + context["forms"] = list(forms_dict.values()) + context["form"] = form + return self.render_to_response(context) diff --git a/src/manage.py b/src/manage.py index c797c8b..0c33ae9 100644 --- a/src/manage.py +++ b/src/manage.py @@ -9,7 +9,7 @@ def main(): """Run administrative tasks.""" os.environ.setdefault( - 'DJANGO_SETTINGS_MODULE', "config.settings.dev") + 'DJANGO_SETTINGS_MODULE', config('DJANGO_SETTINGS_MODULE')) try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/src/media/x.csv b/src/media/x.csv deleted file mode 100644 index 4f195ac..0000000 --- a/src/media/x.csv +++ /dev/null @@ -1,344 +0,0 @@ -email -abdulwasiikhadijah@gmail.com -oyibopraisegod8@gmail.com -doladapo39@gmail.com -duwagbale07@gmail.com -victofunk21@gmail.com -aderoyinayo07@gmail.com -leni4c.dev@gmail.com -olawalekoladefavour@gmail.com -padekoba@gmail.com -mayowamakinde23@gmail.com -adetunjiolufemijoe@gmail.com -davidolukayode2@gmail.com -akinpeluakinola1@gmail.com -davejoshreal@gmail.com -biggiefrosh@gmail.com -solomonfummy@gmail.com -pasedaiyanuolwa@gmail.com -salimumaddeino14@gmail.com -adejireadegite@gmail.com -sammyadetoye@gmail.com -motunpeculiar@gmail.com -alfredfaith35@gmail.com -olanipekunrhoda@gmail.com -ikeoluwamakinwa73@gmail.com -abdulhakeemyussuf105@gmail.com -emmanuelagboola303@gmail.com -peterauduarms@gmail.com -enochola100@gmail.com -mercymujor@gmail.com -idowublessingj@gmail.com -idowublessingj@gmail.com -aopeyemi072@gmail.com -damilolaawoniran0319@gmail.com -ogundeletimothy360@gmail.com -olatujaoluwaseuncomfort@gmail.com -ayomideadeoye5237@gmail.com -08062850775i@gmail.com -ajokekanbi@gmail.com -heritagesam2013@gmail.com -ajibolamatilda@gmail.com -oluborodevictor110@gmail.com -sammydrillz@gmail.com -tanimomoemmanuel@gmail.com -fidelwole@gmail.com -rodiat815@gmail.com -olamilekandaniel076@gmail.com -adakoleandrew21@gmail.com -xdrhblck@gmail.com -bolarinwadolapo247@gmail.com -ademolaalaba133@gmail.com -olakunleibitoye99@gmail.com -olumideolubosede@gmail.com -quadrinahim9@gmail.com -erumiprosper@gmail.com -oluwadaredorismodupe@gmail.com -ayodejishoga1@gmail.com -adewoleabdulazeez4@gmail.com -olayinkafutty007@gmail.com -sundayg423@gmail.com -ilesanmikolawolee@gmail.com -kymsoftly@gmail.com -sarrstevex@gmail.com -osobaolanrewaju1722@gmail.com -chidiebereangel990@gmail.com -isaolawale4@gmail.com -chukwuajahsabastinedev@gmail.com -timadegboye@gmail.com -ogahnina@gmail.com -timothy.olu.ojo@gmail.com -akinbioyinloluwa@gmail.com -afolamianuoluwapo@gmail.com -olayemieniola321@gmail.com -oluwatobi573@gmail.com -emmaayetan@gmail.com -adiomunirah57@gmail.com -olumomiesther@gmail.com -gbemisolapeace391@gmail.com -brainiachades@gmail.com -qasimrokeeb@gmail.com -miracleadepoju71@gmail.com -pelumilowo@gmail.com -emmanuelid2020@gmail.com -olamilekansam006@gmail.com -iammoses19@gmail.com -emmanuelajibokun9@gmail.com -arowolowahab8@gmail.com -arunaaremun728@gmail.com -tobilobaisreal5@gmail.com -obadaregideon@gmail.com -abiodunade204@gmail.com -mzscripterx5@gmail.com -awwalmustapha41@gmail.com -ibrahimabdulquadri446@gmail.com -ogunsprisy@gmail.com -dadaayomikun11@gmail.com -victorferanmi258@gmail.com -agada838@gmail.com -adubiesuemmanuel@gmail.com -ogunsgen4@gmail.com -dosuprecious87@gmail.com -abioyegreat5@gmail.com -oghenetefa@gmail.com -bolu4ril@gmail.com -ogunremis25@gmail.com -preciousbukunmi07@gmail.com -tobiakin89@gmail.com -okerekefavour96@gmail.com -ejalonibuoluwaseyi@gmail.com -jaygadi13js@gmail.com -mide00237@gmail.com -obariomo@gmail.com -ogunodemarvellous39@gmail.com -ayobamiahmed61@gmail.com -benitaadakeja@gmail.com -showidrees@gmail.com -dapopeace21@gmail.com -geniuspeter96@gmail.com -opeyemiawwal2006@gmail.com -zainabajisafe06@gmail.com -ogunmokunademijucaleb@gmail.com -oluseyeoloruntoba@gmail.com -michealayomide625@gmail.com -dejeremiaholorunyomi@gmail.com -olanehemiah20@gmail.com -ebunoke18@gmail.com -ballicesse@gmail.com -babatopehannah2019@gmail.com -michealonu20@gmail.com -abidogunabidemi52@gmail.com -akinwoleolatope@gmail.com -stellaonyinye80@gmail.com -omotoshoopeyemi2007@gmail.com -michealoluwadamilola456@gmail.com -akindoyinamos9@gmail.com -bellorejoice839@gmail.com -funmilayobisola12@gmail.com -adeyemoesther200@gmail.com -olaniyanbenny01234@gmail.com -contacthezron@gmail.com -apataoyinlade@gmail.com -paulfadayo@gmail.com -enochjulius18@gmail.com -ologunjosh2007@gmail.com -jesubamiseakinyoola@gmail.com -motunrayoakanji1@gmail.com -pelemodesmond14@gmail.com -mazidefied@gmail.com -saheedadedipe@gmail.com -lawrenceoyeniran03@gmail.com -olalusiiyanuoluwa4@gmail.com -danieladebisi77@gmail.com -akingbehinben@gmail.com -easyrilwan@gmail.com -sololadavid4@gmail.com -jumbojnr9@gmail.com -adesanmiadeola18@gmail.com -bakare1234@gmail.com -wealthank2017@gmail.com -motunrayob00@gmail.com -floraibeakaeze@gmail.com -ahmadadewumi@gmail.com -olusegundolapo123@gmail.com -fasasitoluwalase754@gmail.com -danielbrightmonday@gmail.com -dave.400g@gmail.com -adesokanadedeji10@gmail.com -denyefaagbede468@gmail.com -abrahamfolorunso6@gmail.com -akinwandeoyewusi@gmail.com -sunrisetoxscyne777@gmail.com -jattodare002@gmail.com -iamseyisamuel@gmail.com -issactoluwanimi@gmail.com -marhamyusuff24@gmail.com -olanrewajuopeyemi218@gmail.com -idrisade430@gmail.com -timileyindgreat02@gmail.com -ifeolufaro@gmail.com -nadiratraji8806@gmail.com -abdulsemiumaryam60@gmail.com -akinkunmioladosu433@gmail.com -vicolraj@gmail.com -oyeleyebal@gmail.com -bellorejoice839@gmail.com -atopeojo@gmail.com -obariomo@gmail.com -perfectpamilerin@gmail.com -hashrafdeen@gmail.com -mayorfalomo@gmail.com -eyitaina@gmail.com -adexbolaji100@gmail.com -idowujohn248@gmail.com -moyinoluwagrace691@gmail.com -mohammedkamaldeen204@gmail.com -miracle.bonnke@gmail.com -bolajiajiku1111@gmail.com -ogunyemioluwatomilola294@gmail.com -abimbolajoy05@gmail.com -ifeoluwaadebobajo123@gmail.com -ayomoobase51@gmail.com -kehindeolurotimi036@gmail.com -alamuelijah186@gmail.com -olamilekanakintilebo@gmail.com -hammedlawal09@gmail.com -oluwaseunsamuel996@gmail.com -abikedesigns@gmail.com -ewakristiakande01@gmail.com -pasedaiyanuolwa@gmail.com -openthefloggage@gmail.com -victormoseri2005@gmail.com -olatejubabalola4@gmail.com -ogunodemarvellous39@gmail.com -olatujaoluwaseuncomfort@gmail.com -ceo.skyhost@gmail.com -oluwadamilola013@gmail.com -oluwaseunagbamu@gmail.com -graceesther04ever@gmail.com -emechetachristian@gmail.com -ibu809829@gmail.com -rachealomololafalowo@gmail.com -egbayeloayodele72@gmail.com -jaygadi13js@gmail.com -emmanuelaajayi2@gmail.com -dahunsiayomiku@gmail.com -adegboyeoluwatosin98@gmail.com -fagbohunkadominion@gmail.com -samuelamos190@gmail.com -blessingadeniyi793@gmail.com -faithakinboyejo@gmail.com -onitamicheal@gmail.com -okohhenrietta57@gmail.com -gifteleojoonoja@gmail.com -calebclintonadeosun@gmail.com -nwankwokris5@gmail.com -ayomiolaniyan@gmail.com -isaaccassij.omo@gmail.com -sundayk180@gmail.com -maryfamuagun@gmail.com -olayiwolaagbejenbi@gmail.com -adebulejohnfav@gmail.com -olanrewajuopeyemi218@gmail.com -idrisade430@gmail.com -adetoyesesamson@gmail.com -iamirewandemichael@gmail.com -olawunmisamuel104@gmail.com -isewhite1234@gmail.com -peterdarasimiaiku@gmail.com -fakeyejoshua2005@gmail.com -radmakson@gmail.com -lawalgbolahan042@gmail.com -sireadrieldaniel@gmail.com -sololadavid4@gmail.com -koladeodunope@gmail.com -nunsiomi@gmail.com -oyetunjiebunoluwa90@gmail.com -obediencepraise@gmail.com -ademidekoya88@gmail.com -doskiano109@gmail.com -shaymah2002@gmail.com -bakare1234@gmail.com -ojo25879@gmail.com -paulblackk35@gmail.com -imadiyi369@gmail.com -edohblessing98@gmail.com -omoyeniglic@gmail.com -fortuneihean0314@gmail.com -ayoeze191@gmail.com -alamuelijah186@gmail.com -peaceiyanu3010@gmail.com -dosuprecious87@gmail.com -merryola90@gmail.com -akinjogunlamayowa@gmail.com -okeyodedivine2006@gmail.com -adejuwonevidence181@gmail.com -mayowamakinde23@gmail.com -bolajis816@gmail.com -adedejirilwan04@gmail.com -dominioniseoluwa74@gmail.com -abisolamofolasayo@gmail.com -faruqalade6@gmail.com -bamideleisaac2106@gmail.com -adejireadegite@gmail.com -koladekuseju@gmail.com -tomoyeted@gmail.com -abdulhakeemyussuf105@gmail.com -iwayemikehinde1@gmail.com -rahmanomoloja@gmail.com -oluwatofunmijoel765@gmail.com -meshbola15@gmail.com -gbemisolapeace391@gmail.com -oluwasemilogodurojaye@gmail.com -ayomiolaniyan@gmail.com -ibidapoayomide754@gmail.com -kylewebdev0@gmail.com -ajayimichael150@gmail.com -joylafenwa@gmail.com -siyanbolahanifah@gmail.com -toluwanieazi@gmail.com -tobiakin89@gmail.com -okanlawonhammad2018@gmail.com -iamfaymousaisida@gmail.com -femmy0128@gmail.com -josephajayi589@gmail.com -damilareodedina24@gmail.com -olumideologundudu39@gmail.com -croesus245@gmail.com -lekaz22691@gmail.com -olotudav22@gmail.com -abdulrahmanibrahim1405@gmail.com -ibeachusichinazaekpere@gmail.com -ebunoke18@gmail.com -bankolesamuel051@gmail.com -diamondigitals@gmail.com -ifeajagunla@gmail.com -odebodezion@gmail.com -adegboyegunmayowa@gmail.com -victoriamichaels247@gmail.com -boladimeji834@gmail.com -benjaminobadare00@gmail.com -feranmiolasupo203@gmail.com -dondanzy31@gmail.com -deelad25@gmail.com -danieloluwanifesimi9@gmail.com -iwalokun572@gmail.com -kishorekumar200417@gmail.com -iyanujehovahb@gmail.com -dharmielaex@gmail.com -mide00237@gmail.com -akinwoleolatope@gmail.com -boladimeji834@gmail.com -felixgogodae777@gmail.com -depopf89@gmail.com -oluwakewasolaolukoya@gmail.com -mvmonterio@gmail.com -oahdevtech@gmail.com -samueltaiwo856@gmail.com -francisgbohunmi@gmail.com -ejalonibuoluwaseyi@gmail.com -olumideolubosede@gmail.com -johnsonelijahbabs@gmail.com -ajiboyeprecious780@gmail.com -ebinesh200416@gmail.com -damolasanya05@gmail.com \ No newline at end of file diff --git a/src/messenger/email_manager.py b/src/messenger/email_manager.py index 194764b..364eb5b 100644 --- a/src/messenger/email_manager.py +++ b/src/messenger/email_manager.py @@ -6,20 +6,19 @@ import requests from django.conf import settings -from utils.general import is_success -from utils.loggers import logger +from messenger.smtp import EmailConnection, get_connection +from utils.general import bytes_to_base64_str, is_success +from utils.loggers import logger, err_logger from .sender_manager import BaseSenderManager class BaseEmailManager(BaseSenderManager): - def __init__( - self, reply_email: str = None, *args, **kwargs - ) -> None: + def __init__(self, reply_email: str = None, *args, **kwargs) -> None: self.__reply_email = reply_email super().__init__(*args, **kwargs) - def get_receipient_field(self) -> str: + def get_recipient_field(self) -> str: return settings.RECEIPIENT_EMAIL_KEY def get_reply_email(self) -> str: @@ -35,17 +34,12 @@ def get_reply_email(self) -> str: class SendGridEmailManager(BaseEmailManager): - @overload def __init__( - self, api_key: str, sender: str, debug: bool, - block_send: bool, reply_email: str - ) -> None: - ... + self, api_key: str, sender: str, debug: bool, block_send: bool, reply_email: str + ) -> None: ... - def __init__( - self, api_key: str, *args, **kwargs - ) -> None: + def __init__(self, api_key: str, *args, **kwargs) -> None: """ SendGrid email manager @@ -72,9 +66,7 @@ def set_headers(self) -> dict: :return: headers :rtype: dict """ - headers = { - 'Authorization': f'Bearer {self.get_api_key()}' - } + headers = {"Authorization": f"Bearer {self.get_api_key()}"} return headers def get_headers(self) -> dict: @@ -86,9 +78,7 @@ def get_headers(self) -> dict: """ return self.__headers - def get_post_data( - self, email: str, subject: str, message: str - ) -> Dict[str, str]: + def get_post_data(self, email: str, subject: str, message: str) -> Dict[str, str]: """ Get post data for sendgrid @@ -106,7 +96,7 @@ def get_post_data( "from": {"email": self.get_sender()}, "reply_to": {"email": self.get_reply_email()}, "subject": subject, - "content": [{"type": "text/html", "value": message}] + "content": [{"type": "text/html", "value": message}], } return data @@ -117,15 +107,13 @@ def get_post_url(self) -> str: :return: post url :rtype: str """ - return 'https://api.sendgrid.com/v3/mail/send' + return "https://api.sendgrid.com/v3/mail/send" - def send( - self, receipient: str, subject: str, message: str, **kwargs - ): + def send(self, recipient: str, subject: str, message: str, **kwargs): response = requests.post( url=self.get_post_url(), - json=self.get_post_data(receipient, subject, message), - headers=self.get_headers() + json=self.get_post_data(recipient, subject, message), + headers=self.get_headers(), ) stat = is_success(response.status_code) if not stat and self.get_debug(): @@ -135,17 +123,12 @@ def send( class ZeptoEmailManager(BaseEmailManager): - @overload def __init__( - self, api_key: str, sender: str, debug: bool, - block_send: bool, reply_email: str - ) -> None: - ... + self, api_key: str, sender: str, debug: bool, block_send: bool, reply_email: str + ) -> None: ... - def __init__( - self, api_key: str, *args, **kwargs - ) -> None: + def __init__(self, api_key: str, *args, **kwargs) -> None: """ Zepto email manager @@ -172,9 +155,7 @@ def set_headers(self) -> dict: :return: headers :rtype: dict """ - headers = { - 'Authorization': self.get_api_key() - } + headers = {"Authorization": self.get_api_key()} return headers def get_headers(self) -> dict: @@ -187,7 +168,7 @@ def get_headers(self) -> dict: return self.__headers def get_post_data( - self, email: str, subject: str, message: str + self, email: str, subject: str, message: str, **kwargs ) -> Dict[str, str]: """ Get post data for zeptomail @@ -201,11 +182,20 @@ def get_post_data( :return: post data :rtype: Dict[str, str] """ + attachments = kwargs.get("attachments", []) data = { "to": [{"email_address": {"address": email}}], "from": {"address": self.get_sender()}, "subject": subject, - "htmlbody": message + "htmlbody": message, + "attachments": [ + { + "name": _["filename"], + "content": bytes_to_base64_str(_["data"]), + "mime_type": _["mime_type"], + } + for _ in attachments + ], } return data @@ -216,20 +206,81 @@ def get_post_url(self) -> str: :return: post url :rtype: str """ - return 'https://api.zeptomail.com/v1.1/email' + return "https://api.zeptomail.com/v1.1/email" - def send( - self, receipient: str, subject: str, message: str, **kwargs - ): - print(self.get_headers()) + def send(self, recipient: str, subject: str, message: str, **kwargs): response = requests.post( url=self.get_post_url(), - json=self.get_post_data(receipient, subject, message), - headers=self.get_headers() + json=self.get_post_data(recipient, subject, message, **kwargs), + headers=self.get_headers(), ) - print(response.json()) stat = is_success(response.status_code) if not stat and self.get_debug(): logger.debug(response.content) logger.debug(response.status_code) return stat + + +class SmtpEmailManager(BaseEmailManager): + @overload + def __init__( + self, + host: str, + port: int, + username: str, + password: str, + sender: str, + debug: bool, + block_send: bool, + reply_email: str, + ) -> None: ... + + def __init__( + self, host: str, port: int, username: str, password: str, *args, **kwargs + ) -> None: + """ + SMTP email manager + + :param host: host + :type host: str + :param port: port + :type port: int + :param username: username + :type username: str + :param password: password + :type password: str + """ + super().__init__(*args, **kwargs) + self.__host = host + self.__port = port + self.__username = username + self.__password = password + + @property + def conn(self) -> EmailConnection: + """ + Get SMTP connection + """ + return get_connection( + host=self.__host, + port=self.__port, + username=self.__username, + password=self.__password, + ) + + def send( + self, recipient: str, subject: str, message: str, attachments=None, **kwargs + ): + try: + self.conn.send( + subject=subject, + recipient=recipient, + text=message, + sender=self.get_sender(), + html=message, + attachments=attachments, + ) + except Exception as e: + err_logger.exception(e) + return False + return True diff --git a/src/messenger/messager.py b/src/messenger/messager.py index 159a590..10a1134 100644 --- a/src/messenger/messager.py +++ b/src/messenger/messager.py @@ -54,12 +54,12 @@ def run_checks(self): class BaseMessenger: def __init__( self, start: int = 0, stop: int = 0, - receipeint_field: str = None + recipient_field: str = None ) -> None: self.__start = start self.__stop = stop self.__manager: Type[Managers] = None - self.__receipeint_field = receipeint_field + self.__recipient_field = recipient_field self.set_manager() self.run_checks() @@ -81,16 +81,16 @@ def get_stop(self) -> int: """ return self.__stop - 1 - def get_receipeint_field(self) -> str: + def get_recipient_field(self) -> str: """ Get receipeint field key :return: receipeint field key :rtype: str """ - if self.__receipeint_field is None: - return self.get_manager().sender_manager.get_receipient_field() - return self.__receipeint_field + if self.__recipient_field is None: + return self.get_manager().sender_manager.get_recipient_field() + return self.__recipient_field def run_checks(self) -> None: if not isinstance(self.__start, int): @@ -313,8 +313,8 @@ def get_index_dict(self, index: int) -> dict: """ return self.data.loc[index].to_dict() - def get_receipient_from_data(self, data: dict) -> str: - key = self.get_receipeint_field() + def get_recipient_from_data(self, data: dict) -> str: + key = self.get_recipient_field() value = data.get(key) if value is None: raise TypeError(f'{key.capitalize()} column not in file') @@ -322,7 +322,7 @@ def get_receipient_from_data(self, data: dict) -> str: def send_messages( self, subject: str, message: str, - context: dict = None + context: dict = None, **kwargs ): if context is None: context = {} @@ -332,9 +332,10 @@ def send_messages( context['message'] = _message _message = self.get_manager()\ .message_manager.render_message(context) - receipient = self.get_receipient_from_data(data) + recipient = self.get_recipient_from_data(data) sent = self.get_manager().sender_manager.send_message( _message, subject=subject, - receipient=receipient + recipient=recipient, + **kwargs ) yield sent diff --git a/src/messenger/sender_manager.py b/src/messenger/sender_manager.py index 7136808..0a3e093 100644 --- a/src/messenger/sender_manager.py +++ b/src/messenger/sender_manager.py @@ -14,11 +14,11 @@ def __init__( self.__debug = debug self.__block_send = block_send - def get_receipient_field(self) -> str: + def get_recipient_field(self) -> str: """ - Get receipient key + Get recipient key """ - raise NotImplementedError('No receipient key') + raise NotImplementedError('No recipient key') def set_debug(self) -> None: self.__debug = True diff --git a/src/messenger/sms_manager.py b/src/messenger/sms_manager.py index 6cc248a..6903626 100644 --- a/src/messenger/sms_manager.py +++ b/src/messenger/sms_manager.py @@ -36,17 +36,17 @@ def __init__( super().__init__(*args, **kwargs) self.client = Client(sid, token) - def get_receipient_field(self) -> str: + def get_recipient_field(self) -> str: return settings.RECEIPIENT_SMS_KEY def send( - self, receipient: str, message: str, **kwargs + self, recipient: str, message: str, **kwargs ) -> bool: """ - Send SMS message to receipient using Twilio + Send SMS message to recipient using Twilio - :param receipient: receipient phone number (e.g. +1234567890) - :type receipient: str + :param recipient: recipient phone number (e.g. +1234567890) + :type recipient: str :param message: message to send :type message: str :return: success of sending message @@ -55,6 +55,6 @@ def send( message = self.client.messages.create( body=message, from_=self.get_sender(), - to=receipient + to=recipient ) return True diff --git a/src/messenger/smtp.py b/src/messenger/smtp.py new file mode 100644 index 0000000..fb935c1 --- /dev/null +++ b/src/messenger/smtp.py @@ -0,0 +1,120 @@ +import smtplib +import ssl +from contextlib import ContextDecorator +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email import encoders +from email.mime.base import MIMEBase +from utils.loggers import err_logger, logger # noqa + + +class SingletonMeta(type): + """ + A metaclass for creating a singleton class. + """ + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + instance = super().__call__(*args, **kwargs) + cls._instances[cls] = instance + return cls._instances[cls] + +class EmailConnection(ContextDecorator, metaclass=SingletonMeta): + """ + This class is responsible for connecting to the + email server and sending the email. + """ + + def __init__(self, host: str, port: int, username: str, password: str): + self.host = host + self.port = port + self.username = username + self.password = password + self.connection = None + self.context = ssl.create_default_context() + + + def __enter__(self): + if self.connection is not None: + logger.info("Connection already exists") + return self + logger.info("Connecting to the email server") + if self.port == 465: + self.connection = smtplib.SMTP_SSL( + self.host, self.port, context=self.context + ) + else: + self.connection = smtplib.SMTP(self.host, self.port) + self.connection.starttls(context=self.context) + logger.info("Logging into the email server") + self.connection.login(self.username, self.password) + logger.info("Successfully logged in") + return self + + def __exit__(self, *exc): + logger.info("Disconnecting from the email server") + self.connection.quit() + + def send( + self, + subject: str, + recipient: str, + text: str, + sender: str, + html: str = None, + attachments: list[dict[str, bytes | str]] = None, + ): + """ + Send email + + :param subject: Email subject + :type subject: str + :param recipient: Email recipient + :type recipient: str + :param text: Email text + :type text: str + :param sender: Email sender + :type sender: str + :param html: Email html, defaults to None + :type html: str, optional + :param attachments: Email attachments in the form of a list of dictionaries of the form {"filename": str, "data": bytes}, defaults to None + :type attachments: list[dict[str, bytes | str]], optional + :return: True if email is sent successfully else raise exception + :rtype: bool + """ + message = MIMEMultipart("alternative") + message["Subject"] = subject + message["From"] = sender + message["To"] = recipient + + message.attach(MIMEText(text, "plain")) + if html is not None: + message.attach(MIMEText(html, "html")) + + if attachments is not None: + logger.info("Adding attachments to the email") + for attachment in attachments: + part = MIMEBase("application", "octet-stream") + part.set_payload(attachment["data"]) + encoders.encode_base64(part) + filename = attachment["filename"] + part.add_header( + "Content-Disposition", + f"attachment; filename= {filename}", + ) + message.attach(part) + + try: + self.connection.sendmail(self.username, recipient, message.as_string()) + return True + except Exception as e: + err_logger.error(f"Failed to send email: {e}") + err_logger.exception(e) + raise e + + +def get_connection(host: str, port: int, username: str, password: str): + connection = EmailConnection(host, port, username, password) + connection.__enter__() + return connection diff --git a/src/messenger/tests/test_email_manager.py b/src/messenger/tests/test_email_manager.py index 91985d9..87817a5 100644 --- a/src/messenger/tests/test_email_manager.py +++ b/src/messenger/tests/test_email_manager.py @@ -14,8 +14,7 @@ class TestManager(SimpleTestCase): def setUp(self) -> None: manager = BaseEmailManager( - domain='example.com', - sender='test', + sender='test@example.com', ) self.manager = manager @@ -108,8 +107,7 @@ class TestSendGridManager(SimpleTestCase): def setUp(self) -> None: manager = SendGridEmailManager( - domain='example.com', - sender='test', + sender='test@example.com', api_key='test_key', reply_email='noreply@test.io' ) @@ -154,15 +152,16 @@ def test_get_post_url(self): @pytest.mark.xfail def test_send_email(self): - computed = self.manager.send_email( - email='me@gmail.com', + computed = self.manager.send( + recipient='me@gmail.com', subject='testings', message='hello world', ) self.assertFalse(computed) def test_send_email_fail_silently(self): - computed = self.manager.send_email( + computed = self.manager.send( + recipient='me@gmail.com', subject='testings', message='hello world', ) @@ -170,7 +169,7 @@ def test_send_email_fail_silently(self): def test_send_email_not_fail_silently(self): with self.assertRaises(Exception): - self.manager.send_email( + self.manager.send( subject='testings', message='hello world', fail=False diff --git a/src/messenger/tests/test_messenger.py b/src/messenger/tests/test_messenger.py index fac6820..c9518cf 100644 --- a/src/messenger/tests/test_messenger.py +++ b/src/messenger/tests/test_messenger.py @@ -16,18 +16,18 @@ U = Type[TestCase] -def test_set_email_manager(managers: M, email_manager: E): - managers.set_email_manager(email_manager) - assert email_manager is managers.email_manager +def test_set_sender_manager(managers: M, sender_manager: E): + managers.set_sender_manager(sender_manager) + assert sender_manager is managers.sender_manager -def test_set_email_manager_error(managers: M): +def test_set_sender_manager_error(managers: M): with pytest.raises(TypeError): - managers.set_email_manager('wrong type') + managers.set_sender_manager('wrong type') -def test_email_manager(managers: M): - assert managers.email_manager is None +def test_sender_manager(managers: M): + assert managers.sender_manager is None def test_set_message_manager(managers: M, message_manager: T): @@ -52,12 +52,12 @@ def test_run_checks_email(managers: M): managers.run_checks() -def test_run_checks_message(managers: M, email_manager: E): +def test_run_checks_message(managers: M, sender_manager: E): """ raise error for message manager check """ with pytest.raises(NotImplementedError): - managers.set_email_manager(email_manager) + managers.set_sender_manager(sender_manager) managers.run_checks() @@ -100,8 +100,8 @@ def test_base_manager_set_message_manager(messenger: B, message_manager): messenger.set_message_manager(message_manager) -def test_base_manager_set_email_manager(messenger: B, email_manager): - messenger.set_email_manager(email_manager) +def test_base_manager_set_sender_manager(messenger: B, sender_manager): + messenger.set_sender_manager(sender_manager) def test_base_manager_get_message_without_data(messenger: B): diff --git a/src/pytest.ini b/src/pytest.ini index fbbb260..941c1f5 100644 --- a/src/pytest.ini +++ b/src/pytest.ini @@ -1,3 +1,3 @@ [pytest] -DJANGO_SETTINGS_MODULE = dry.settings.test +DJANGO_SETTINGS_MODULE = config.settings.test addopts = --disable-pytest-warnings --cov=. --cov-report term \ No newline at end of file diff --git a/src/tox.ini b/src/tox.ini index 808ae33..c98c57c 100644 --- a/src/tox.ini +++ b/src/tox.ini @@ -1,7 +1,7 @@ [coverage:run] omit = */tests/* manage.py - dry/* + src/* */tests* [coverage:report] diff --git a/src/utils/general.py b/src/utils/general.py index 6450c81..fca2833 100644 --- a/src/utils/general.py +++ b/src/utils/general.py @@ -1,6 +1,7 @@ """General python utilities to be used in project""" +import base64 from typing import Any, Iterable @@ -18,3 +19,6 @@ def count_true_in_iter(iter): def is_success(code): return 200 <= code <= 299 + +def bytes_to_base64_str(data: bytes) -> str: + return base64.b64encode(data).decode('utf-8') diff --git a/test.html b/test.html new file mode 100644 index 0000000..79d8dd0 --- /dev/null +++ b/test.html @@ -0,0 +1,109 @@ + + + + + + Email page + + + + + +
+

+ + + + +
+ + + \ No newline at end of file