Skip to content

Commit

Permalink
minor updates and bug fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulleDemon committed Oct 20, 2023
1 parent 561c350 commit d3ce183
Show file tree
Hide file tree
Showing 17 changed files with 178 additions and 53 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ test.py
__pycache__
.env
db.sqlite3
firebase-cred.json
media/
42 changes: 40 additions & 2 deletions automail/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,41 @@

from django_celery_beat.models import PeriodicTask, ClockedSchedule, IntervalSchedule


from .models import EmailCampaignTemplate
from utils.tasks import send_html_mail_celery
from .models import EmailCampaignTemplate, EmailConfiguration, EmailCampaign


@receiver(post_save, sender=EmailCampaignTemplate)
def schedule_email(instance, sender, created, *args, **kwargs):

if instance.scheduled:

tasks = PeriodicTask.objects.filter(name=f'email_{instance.id}')
tasks.update(enabled=False)
tasks.delete()

schedule_start = ClockedSchedule.objects.create(clocked_time=instance.schedule)

PeriodicTask.objects.create(name=f'email_{instance.id}', clocked=schedule_start, one_off=True,
kwargs=json.dumps({'id': instance.id}),
task='run_schedule_email')

else:
tasks = PeriodicTask.objects.filter(name=f'email_{instance.id}')
tasks.update(enabled=False)
tasks.delete()


@receiver(post_save, sender=EmailCampaign)
def disable_email_campaign(instance, sender, created, *args, **kwargs):

if instance.discontinued == True:
for x in EmailCampaignTemplate.objects.filter(campaign=instance.id):
tasks = PeriodicTask.objects.filter(name=f'email_{x.id}')
tasks.update(enabled=False)
tasks.delete()


@receiver(pre_delete, sender=EmailCampaignTemplate)
def remove_scheduled_mail(sender, instance, **kwargs):

Expand All @@ -29,3 +50,20 @@ def remove_scheduled_mail(sender, instance, **kwargs):

except (PeriodicTask.DoesNotExist):
pass


@receiver(post_save, sender=EmailConfiguration)
def inform_user(sender, instance, created, *args, **kwargs):

if created:

name, _ = instance.email.split("@")

subject = "Your mail has been found to be use"
body = f"Hi {name.replace('.', ' ')},\nWe found the email '{instance.email}' being used on " + \
"AtMailWin website for email automation. If this wasn't done by you, we strongly suggest you change your email's password and " + \
"report to us on this <a href='https://github.com/PaulleDemon/Email-automation/issues'>page</a>. You can ignore this email if this was done by you."+\
"\n\nBest regards,\nAtMailWin Team"

send_html_mail_celery.delay(subject, message=body, html_message=body, recipient_list=[instance.email])

2 changes: 1 addition & 1 deletion automail/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

path('campaigns/', campaigns_view, name='email-campaigns'),
path('campaign/create/', campaign_create_view, name='email-campaign-create'),
path('campaign/<int:id>/delete/', campaign_create_view, name='email-campaign-delete'),
path('campaign/<int:id>/delete/', delete_campaign_view, name='email-campaign-delete'),

path('configure/add/', configuration_create_view, name='configure-email'),
path('configure/<int:id>/delete/', delete_configuration_view, name='configure-email-delete'),
Expand Down
4 changes: 2 additions & 2 deletions automail/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ def campaigns_view(request):
def delete_campaign_view(request, id):

try:
EmailCampaign.objects.get(user=request.user.id, id=id)
EmailCampaign.objects.get(user=request.user.id, id=id).delete()

except (EmailCampaign.DoesNotExist):
return render(request, "404.html")
Expand Down Expand Up @@ -439,7 +439,7 @@ def configuration_create_view(request):

credentials = form.cleaned_data
credentials_valid, error = test_email_credentials(email=credentials['email'], password=credentials['password'],
host=credentials['host'], port=credentials['port'])
host=credentials['host'], port=credentials['port'], imap_host=credentials['imap_host'])

if credentials_valid != True:
return render(request, 'configure-server.html', context={'errors': [error], 'configuration': credentials})
Expand Down
24 changes: 17 additions & 7 deletions email_automation/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
ALLOWED_HOSTS = []

else:
ALLOWED_HOSTS = ['localhost:8000', ]
ALLOWED_HOSTS = env('ALLOWED_PROD_HOSTS').replace(' ', '').split(',')


if DEBUG:
DOMAIN = 'http://localhost:8000'
Expand Down Expand Up @@ -98,6 +99,9 @@
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TASK_SERIALIZER = 'json'

CELERYBEAT_SCHEDULER = 'django_celery_beat.schedulers.DatabaseScheduler'
CELERY_IMPORTS = ['utils.tasks',]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
Expand Down Expand Up @@ -136,7 +140,7 @@
# EMAIL_USE_TLS = True
EMAIL_USE_SSL = True

EMAIL_FROM_SIGNATURE = 'Best regards, Team'
EMAIL_FROM_SIGNATURE = 'Best regards, Atmailwin Team'
EMAIL_FROM = 'info@atmailwin.com'
EMAIL_FROM_NAME = 'AtMailWin'

Expand Down Expand Up @@ -228,7 +232,13 @@
STATIC_ROOT = BASE_DIR.joinpath('staticfiles', 'static')

MEDIA_ROOT = BASE_DIR.joinpath('media')
MEDIA_URL = '/media/'

if DEBUG:
MEDIA_URL = '/media/'
MEDIA_DOMAIN = 'http://localhost:8000'

else:
MEDIA_URL = '/media/'

# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
Expand Down Expand Up @@ -256,10 +266,10 @@
'class': 'logging.StreamHandler',
},

'celery': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
},
# 'celery': {
# 'level': 'DEBUG',
# 'class': 'logging.StreamHandler',
# },

# Send info messages to syslog
'syslog':{
Expand Down
3 changes: 2 additions & 1 deletion email_automation/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@
path('email/', include('automail.urls')),
path("__reload__/", include("django_browser_reload.urls")),

re_path(r'^.*/$', view_404, name='page_not_found'),
]
if settings.DEBUG:
urlpatterns += []

urlpatterns += static(settings.MEDIA_URL, document_root = settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

urlpatterns += [ re_path(r'^.*/$', view_404, name='page_not_found'),]
3 changes: 3 additions & 0 deletions logos/atmailwin-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 34 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# Email Automation tool
# Email Automation tool- AtMailWin

<p align="center">
<img src="logos/atmailwin-logo.svg" alt="CupidCues icon" width="300px" height="300px"/>
</p>

A free and open-source email automation tool. Schedule, personalize and send!
<br/>
<br/>

You can use the site at [https://atmailwin.com](https://atmailwin.com)

or
or if you want to self host it.

clone the repo
```
Expand All @@ -18,8 +26,14 @@ add a .env file inside the email_automation folder with the following
```
DEBUG=0
DOMAIN=""
ALLOWED_PROD_HOSTS=""
SECRET_KEY=""
REDIS_PROD_HOST=""
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
FIELD_ENCRYPTION_KEY="" # for encrypting
```
> You must fill up the values required
Expand All @@ -42,4 +56,21 @@ python manage.py tailwind start
Once you are satisfied you can build the tailwind using
```
python manage.py tailwind build
```
```

> Note: This tool makes use of Jinja templatig engine in the backend and nunjucks in the frontend for error checks.
> You can use any valid Jinja syntax in the email template you are creating.
**Support Opensource**

Developing and maintaining open-source and free projects requires a significant commitment of time and effort. My goal is to transition to working on open-source projects on a full-time basis. If you'd like to support me and the open-source community consider making a small donation. You can have your logo/name on this [page](https://atmailwin.com/support/) .


[<img src="https://github.com/PaulleDemon/PaulleDemon/blob/main/images/buy-me-coffee.png?raw=true" height="100px" width="350px">](https://www.buymeacoffee.com/ArtPaul)

**Other ways to support oepn-source**

* To dedicate more time to open-source I am also giving a non-GPL/commercial code license that can be used in a closed source project. Send a mail to paul@adostrings.com

* I also have production scale private projects (dating app, Social media, food delivery and more.), If you are interested in purchase, send a mail to paul@adostrings.com.
14 changes: 9 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@ psycopg2-binary==2.9.3
django-tailwind==3.6.0
django-browser-reload==1.11.0
django-encrypted-model-fields==0.6.5
django-storages==1.14.2
django-cors-headers==4.2.0
django-celery-beat==2.5.0
django-ratelimit==4.1.0

google-cloud-storage==2.12.0

pandas==2.0.3
xlrd==2.0.1
openpyxl==3.1.2

beautifulsoup4==4.12.2

email-validator==2.0.0.post2

celery==5.2.7
redis==5.0.0
django-cors-headers==4.2.0
django-celery-beat==2.5.0

django-ratelimit==4.1.0

requests==2.31

PyJWT==2.8.0

7 changes: 3 additions & 4 deletions staticfiles/static/js/utils/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,11 @@ function isValidEmail(email){
}

function isValidDomain(domain) {
// Regular expression pattern to match a valid domain
const domainPattern = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/
// Regular expression pattern to match a valid domain, including subdomains
const domainPattern = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i;

return domainPattern.test(domain);
}

/**
*
* @param {File} file
Expand Down
4 changes: 2 additions & 2 deletions templates/html/email-product/campaign/campaign-details.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Delete Template</h5>
<h5 class="modal-title" id="exampleModalLabel">Delete Campaign</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="modal-body">
Are you sure you want to delete this? This action cannot be undone
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<form action="" id="modal-btn-delete" method="post">
<form id="modal-btn-delete" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
</form>
Expand Down
4 changes: 3 additions & 1 deletion templates/html/email-product/campaign/email-campaigns.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,11 @@ <h5 class="modal-title" id="exampleModalLabel">Delete Template</h5>
</table>

<div class="tw-flex tw-gap-1 tw-mt-auto tw-text-2xl">
<button type="submit" onclick="onDeleteCampaign('{% url 'email-campaign-delete' id=campaign.id %}', '{{ campaign.name }}')"

<button type="button" onclick="onDeleteCampaign('{% url 'email-campaign-delete' id=campaign.id %}', '{{ campaign.name }}')"
class="btn !tw-text-red-600 tw-w-full" data-bs-toggle="modal" data-bs-target="#delete-modal"><i class="bi bi-trash"></i>
</button>

<a class="btn !tw-border-solid tw-w-full" href="{% url 'email-campaigns' %}?view={{ campaign.id }}"
data-bs-toggle="tooltip" data-bs-placement="bottom" title="Detailed view"
>
Expand Down
11 changes: 7 additions & 4 deletions templates/html/email-product/configuration/configure-server.html
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,13 @@
</div>

<div class="tw-text-md tw-mt-4">
For people using Gmail you will have to enable less secure apps to make this work.
Please refer the following guide
<a href="https://stackoverflow.com/a/27515833/15993687" target="_blank"
rel="noreferrer" class="tw-underline tw-text-blue-600" >How to turn on less secure app?</a>
For people using Gmail please refer the below guide on how to generate an app password
<a href="https://stackoverflow.com/a/73214197/15993687" target="_blank"
rel="noreferrer" class="tw-underline tw-text-blue-600" >Generating app password</a>
</div>

<div class="tw-font-semibold">
The email configurations password and email are encrypted in the backend.
</div>
</div>

Expand Down
2 changes: 1 addition & 1 deletion templates/js/email-product/campaign-create.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function templatePreview(){
if (event.target.value){
// const url = templateViewBtn.getAttribute("url") + `?edit=${event.target.value}`
// templateViewBtn.setAttribute("href", url)
templateViewBtn("onclick", `viewTemplate(${event.target.value})`)
templateViewBtn.setAttribute("onclick", `viewTemplate(${event.target.value})`)
// viewTemplate(event.target.value)

}else{
Expand Down
6 changes: 3 additions & 3 deletions templates/js/utils/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ function isValidEmail(email){
}

function isValidDomain(domain) {
// Regular expression pattern to match a valid domain
const domainPattern = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/
// Regular expression pattern to match a valid domain, including subdomains
const domainPattern = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i;

return domainPattern.test(domain);
}

Expand Down
13 changes: 10 additions & 3 deletions utils/mailing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import time
import random
import smtplib
import imaplib

Expand Down Expand Up @@ -84,15 +85,21 @@ def send_email_with_attachments(subject, text_message, html_message, html_contex
return email.send()


def check_recipient_responded(email, start_date, end_date, imap: imaplib.IMAP4):
def check_recipient_responded(email, start_date, end_date, imap_client: imaplib.IMAP4):
"""
Given a mail id the function test if the
reipient has replied between a specified datetime
"""
imap.select("INBOX")

if settings.DEBUG:
return random.choice([True, False])

imap_client.select("INBOX")
# Search for emails based on sender and date range
search_criteria = f'(FROM "{email}") SINCE "{start_date}" BEFORE "{end_date}"'
result, email_ids = imap.search(None, search_criteria)
result, email_ids = imap_client.search(None, search_criteria)

imap_client.close()

if result == 'OK':
email_ids = email_ids[0].split()
Expand Down
Loading

0 comments on commit d3ce183

Please sign in to comment.