django-outlook-sync 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-outlook-sync
3
+ Version: 1.0.0
4
+ Summary: A standalone reusable Django package to automate background syncing of local desktop Outlook emails.
5
+ Author-email: Yonas <yonas2754@gmail.com>
6
+ Classifier: Framework :: Django
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: Microsoft :: Windows
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pywin32; sys_platform == "win32"
12
+ Requires-Dist: schedule>=1.2.0
13
+ Requires-Dist: djangorestframework>=3.14.0
14
+
15
+ # Django Outlook Sync Engine
16
+
17
+ A reusable Django module to sync structural message items seamlessly from localized Outlook desktop software installations into individual app environments.
18
+
19
+ ## Setup Instructions
20
+ 1. Install your local package.
21
+ 2. Register `'django_outlook_sync'` in your `INSTALLED_APPS`.
22
+ 3. Run `python manage.py migrate`.
@@ -0,0 +1,8 @@
1
+ # Django Outlook Sync Engine
2
+
3
+ A reusable Django module to sync structural message items seamlessly from localized Outlook desktop software installations into individual app environments.
4
+
5
+ ## Setup Instructions
6
+ 1. Install your local package.
7
+ 2. Register `'django_outlook_sync'` in your `INSTALLED_APPS`.
8
+ 3. Run `python manage.py migrate`.
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+ class DjangoOutlookSyncConfig(AppConfig):
4
+ default_auto_field = 'django.db.models.BigAutoField'
5
+ name = 'django_outlook_sync'
6
+ verbose_name = "Django Outlook Sync Engine"
@@ -0,0 +1,52 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import subprocess
5
+ import schedule
6
+ from datetime import datetime
7
+
8
+ def run_django_sync():
9
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
10
+ print(f"[{current_time}] Triggering automated Outlook database sync...")
11
+
12
+ try:
13
+ # Executes within context of target project using active runtime interpreter variables
14
+ result = subprocess.run(
15
+ [sys.executable, "manage.py", "sync_outlook"],
16
+ capture_output=True,
17
+ text=True
18
+ )
19
+
20
+ log_file_path = "sync_log.txt"
21
+ with open(log_file_path, "a") as log:
22
+ log.write(f"--- Sync executed at {current_time} ---\n")
23
+ log.write(result.stdout)
24
+ if result.stderr:
25
+ log.write(f"Errors:\n{result.stderr}")
26
+ log.write("\n")
27
+
28
+ print(f"[{current_time}] Sync pipeline finished successfully.")
29
+
30
+ except Exception as e:
31
+ print(f"[{current_time}] Critical automation failure: {str(e)}")
32
+
33
+ def main():
34
+ if not os.path.exists("manage.py"):
35
+ print("❌ Error: 'manage.py' not found! Open your terminal in a valid Django project root folder to run this engine daemon.")
36
+ sys.exit(1)
37
+
38
+ schedule.every(5).minutes.do(run_django_sync)
39
+ run_django_sync()
40
+
41
+ print("⏰ Outlook background daemon initialized. Keeping alive... (Press Ctrl+C to exit)")
42
+
43
+ while True:
44
+ try:
45
+ schedule.run_pending()
46
+ time.sleep(1)
47
+ except KeyboardInterrupt:
48
+ print("\nStopping Outlook background daemon. Goodbye!")
49
+ sys.exit(0)
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -0,0 +1,19 @@
1
+ from django.core.management.base import BaseCommand
2
+ from django_outlook_sync.outlook import OutlookService
3
+ from django_outlook_sync.sync import SyncService
4
+
5
+ class Command(BaseCommand):
6
+ help = "Sync outlook emails from running desktop application client environment"
7
+
8
+ def handle(self, *args, **options):
9
+ self.stdout.write("Initializing local COM Connection parameters...")
10
+ try:
11
+ outlook = OutlookService()
12
+ messages = outlook.get_messages()
13
+
14
+ self.stdout.write("Running processing pipelines...")
15
+ SyncService().run(messages)
16
+
17
+ self.stdout.write(self.style.SUCCESS("Outlook database sync completed successfully."))
18
+ except Exception as e:
19
+ self.stdout.write(self.style.ERROR(f"Sync operation encountered failures: {str(e)}"))
@@ -0,0 +1,18 @@
1
+ from django.db import models
2
+
3
+ class EmailMessage(models.Model):
4
+ message_id = models.CharField(max_length=255, unique=True)
5
+ subject = models.CharField(max_length=500, blank=True, null=True)
6
+ sender = models.CharField(max_length=255, blank=True, null=True)
7
+ recipients = models.TextField(blank=True, null=True)
8
+ cc_recipients = models.TextField(blank=True, null=True)
9
+ html_body = models.TextField(blank=True, null=True)
10
+ received_at = models.DateTimeField()
11
+ is_thread_reply = models.BooleanField(default=False)
12
+ created_at = models.DateTimeField(auto_now_add=True)
13
+
14
+ class Meta:
15
+ db_table = 'outlook_email_messages'
16
+
17
+ def __str__(self):
18
+ return f"{self.subject or 'No Subject'} - {self.sender or 'Unknown'}"
@@ -0,0 +1,18 @@
1
+ import sys
2
+
3
+ class OutlookService:
4
+ def __init__(self):
5
+ if sys.platform != "win32":
6
+ raise OSError("OutlookService requires a Windows Environment running Microsoft Outlook.")
7
+
8
+ import win32com.client
9
+ import pythoncom
10
+
11
+ pythoncom.CoInitialize()
12
+ self.outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
13
+
14
+ def get_messages(self):
15
+ inbox = self.outlook.GetDefaultFolder(6)
16
+ messages = inbox.Items
17
+ messages.Sort("[ReceivedTime]", True)
18
+ return messages
@@ -0,0 +1,18 @@
1
+ from rest_framework import serializers
2
+ from .models import EmailMessage
3
+
4
+ class EmailMessageSerializer(serializers.ModelSerializer):
5
+ class Meta:
6
+ model = EmailMessage
7
+ fields = [
8
+ 'id',
9
+ 'message_id',
10
+ 'subject',
11
+ 'sender',
12
+ 'recipients',
13
+ 'cc_recipients',
14
+ 'received_at',
15
+ 'is_thread_reply',
16
+ 'created_at',
17
+ 'html_body',
18
+ ]
@@ -0,0 +1,153 @@
1
+ import os
2
+ import re
3
+ from datetime import datetime, timedelta
4
+
5
+ from django.conf import settings
6
+ from django.utils.timezone import make_aware
7
+ from django.db import transaction, IntegrityError
8
+
9
+ from .models import EmailMessage
10
+
11
+
12
+ class SyncService:
13
+
14
+ def __init__(self):
15
+ # Fetch configurations from parent project settings with safe default fallbacks
16
+ self.allowed_recipients = getattr(settings, "OUTLOOK_ALLOWED_RECIPIENTS", [])
17
+ self.auto_reply_template = getattr(
18
+ settings,
19
+ "OUTLOOK_REPLY_TEMPLATE",
20
+ "<p>Hello,</p><p>Your email has been received and processed.</p><p>Thank you.</p>"
21
+ )
22
+
23
+ def is_valid_recipient(self, to_field, cc_field):
24
+ if not self.allowed_recipients:
25
+ return True
26
+
27
+ all_recipients = f"{to_field} {cc_field}".lower()
28
+ return all(name.lower() in all_recipients for name in self.allowed_recipients)
29
+
30
+ def run(self, messages):
31
+ now = datetime.now()
32
+ five_minutes_ago = now - timedelta(minutes=5)
33
+ saved_count = 0
34
+
35
+ output_dir = os.path.join(settings.BASE_DIR, "static", "email_images")
36
+ os.makedirs(output_dir, exist_ok=True)
37
+
38
+ try:
39
+ messages.Sort("[ReceivedTime]", True)
40
+ messages_list = list(messages)
41
+ except Exception as e:
42
+ print(f"Unable to natively sort messages: {e}")
43
+ messages_list = list(messages)
44
+
45
+ print(f"Processing {len(messages_list)} messages")
46
+
47
+ for msg in messages_list:
48
+ try:
49
+ if getattr(msg, "Class", 0) != 43:
50
+ continue
51
+
52
+ message_id = getattr(msg, "EntryID", None)
53
+ if not message_id:
54
+ continue
55
+
56
+ raw_time = getattr(msg, "ReceivedTime", None)
57
+ if not raw_time:
58
+ continue
59
+
60
+ naive_dt = datetime(
61
+ raw_time.year, raw_time.month, raw_time.day,
62
+ raw_time.hour, raw_time.minute, raw_time.second,
63
+ )
64
+
65
+ if naive_dt < five_minutes_ago:
66
+ continue
67
+
68
+ subject = getattr(msg, "Subject", "No Subject")
69
+ sender = getattr(msg, "SenderName", "Unknown Sender")
70
+ received_at = make_aware(naive_dt)
71
+
72
+ to_recipients = getattr(msg, "To", "")
73
+ cc_recipients = getattr(msg, "CC", "")
74
+
75
+ if not self.is_valid_recipient(to_recipients, cc_recipients):
76
+ continue
77
+
78
+ id_exists = EmailMessage.objects.filter(message_id=message_id).exists()
79
+ footprint_exists = EmailMessage.objects.filter(
80
+ sender=sender, subject=subject, received_at=received_at
81
+ ).exists()
82
+
83
+ if id_exists or footprint_exists:
84
+ continue
85
+
86
+ html_body = getattr(msg, "HTMLBody", "")
87
+
88
+ if html_body and "Your email has been received and processed." in html_body:
89
+ print(f"Skipping email '{subject}'—loop protection.")
90
+ continue
91
+
92
+ print(f"Processing New Email: {subject} from {sender}")
93
+
94
+ if html_body:
95
+ html_body = re.sub(
96
+ r"IMPORTANT\..*?actually\s+virus\s+free\s*\.?",
97
+ "", html_body, flags=re.DOTALL | re.IGNORECASE,
98
+ )
99
+ html_body = re.sub(r"<p[^>]*>\s*</p>", "", html_body)
100
+ html_body = re.sub(r"<div[^>]*>\s*</div>", "", html_body)
101
+
102
+ safe_msg_id = re.sub(r"[^a-zA-Z0-9]", "", str(message_id))[-20:]
103
+ attachments = getattr(msg, "Attachments", None)
104
+
105
+ if attachments and attachments.Count > 0:
106
+ for i in range(1, attachments.Count + 1):
107
+ attachment = attachments.Item(i)
108
+ try:
109
+ cid = attachment.PropertyAccessor.GetProperty(
110
+ "http://schemas.microsoft.com/mapi/proptag/0x3712001E"
111
+ )
112
+ except Exception:
113
+ cid = None
114
+
115
+ if cid:
116
+ file_ext = os.path.splitext(attachment.FileName)[1] or ".png"
117
+ filename = f"{safe_msg_id}_{cid}{file_ext}"
118
+ file_path = os.path.join(output_dir, filename)
119
+ attachment.SaveAsFile(file_path)
120
+
121
+ html_body = html_body.replace(
122
+ f"cid:{cid}", f"/static/email_images/{filename}"
123
+ )
124
+
125
+ is_reply = subject and subject.lower().strip().startswith(("re:", "fw:"))
126
+
127
+ try:
128
+ with transaction.atomic():
129
+ EmailMessage.objects.create(
130
+ message_id=message_id,
131
+ subject=subject,
132
+ sender=sender,
133
+ recipients=to_recipients,
134
+ cc_recipients=cc_recipients,
135
+ html_body=html_body,
136
+ received_at=received_at,
137
+ is_thread_reply=is_reply,
138
+ )
139
+
140
+ reply = msg.Reply()
141
+ reply.HTMLBody = self.auto_reply_template + reply.HTMLBody
142
+ reply.Send()
143
+ saved_count += 1
144
+
145
+ except IntegrityError:
146
+ print(f"Duplicate blocked: {subject}")
147
+ continue
148
+
149
+ except Exception as e:
150
+ print(f"Skipping unreadable email: {e}")
151
+ continue
152
+
153
+ print(f"Sync completed: {saved_count} emails processed since {five_minutes_ago}")
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-outlook-sync
3
+ Version: 1.0.0
4
+ Summary: A standalone reusable Django package to automate background syncing of local desktop Outlook emails.
5
+ Author-email: Yonas <yonas2754@gmail.com>
6
+ Classifier: Framework :: Django
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: Microsoft :: Windows
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pywin32; sys_platform == "win32"
12
+ Requires-Dist: schedule>=1.2.0
13
+ Requires-Dist: djangorestframework>=3.14.0
14
+
15
+ # Django Outlook Sync Engine
16
+
17
+ A reusable Django module to sync structural message items seamlessly from localized Outlook desktop software installations into individual app environments.
18
+
19
+ ## Setup Instructions
20
+ 1. Install your local package.
21
+ 2. Register `'django_outlook_sync'` in your `INSTALLED_APPS`.
22
+ 3. Run `python manage.py migrate`.
@@ -0,0 +1,19 @@
1
+ README.md
2
+ pyproject.toml
3
+ django_outlook_sync/__init__.py
4
+ django_outlook_sync/apps.py
5
+ django_outlook_sync/models.py
6
+ django_outlook_sync/outlook.py
7
+ django_outlook_sync/serializers.py
8
+ django_outlook_sync/sync.py
9
+ django_outlook_sync.egg-info/PKG-INFO
10
+ django_outlook_sync.egg-info/SOURCES.txt
11
+ django_outlook_sync.egg-info/dependency_links.txt
12
+ django_outlook_sync.egg-info/entry_points.txt
13
+ django_outlook_sync.egg-info/requires.txt
14
+ django_outlook_sync.egg-info/top_level.txt
15
+ django_outlook_sync/bin/__init__.py
16
+ django_outlook_sync/bin/auto_sync.py
17
+ django_outlook_sync/management/__init__.py
18
+ django_outlook_sync/management/commands/__init__.py
19
+ django_outlook_sync/management/commands/sync_outlook.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ django-outlook-daemon = django_outlook_sync.bin.auto_sync:main
@@ -0,0 +1,5 @@
1
+ schedule>=1.2.0
2
+ djangorestframework>=3.14.0
3
+
4
+ [:sys_platform == "win32"]
5
+ pywin32
@@ -0,0 +1 @@
1
+ django_outlook_sync
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "django-outlook-sync"
7
+ version = "1.0.0"
8
+ authors = [
9
+ { name="Yonas", email="yonas2754@gmail.com" }
10
+ ]
11
+ description = "A standalone reusable Django package to automate background syncing of local desktop Outlook emails."
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ classifiers = [
15
+ "Framework :: Django",
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: Microsoft :: Windows",
18
+ ]
19
+ dependencies = [
20
+ "pywin32; sys_platform == 'win32'",
21
+ "schedule>=1.2.0",
22
+ "djangorestframework>=3.14.0",
23
+ ]
24
+
25
+ [project.scripts]
26
+ django-outlook-daemon = "django_outlook_sync.bin.auto_sync:main"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["."]
30
+ include = ["django_outlook_sync*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+