django-codenerix-email 4.0.35__py2.py3-none-any.whl → 4.0.36__py2.py3-none-any.whl
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.
- codenerix_email/__init__.py +1 -1
- codenerix_email/__pycache__/__init__.cpython-310.pyc +0 -0
- codenerix_email/__pycache__/__init__.cpython-311.pyc +0 -0
- codenerix_email/__pycache__/models.cpython-310.pyc +0 -0
- codenerix_email/__pycache__/models.cpython-311.pyc +0 -0
- codenerix_email/management/commands/__pycache__/email_test.cpython-311.pyc +0 -0
- codenerix_email/management/commands/__pycache__/emails_recv.cpython-311.pyc +0 -0
- codenerix_email/management/commands/__pycache__/emails_send.cpython-311.pyc +0 -0
- codenerix_email/management/commands/__pycache__/recv_emails.cpython-311.pyc +0 -0
- codenerix_email/management/commands/__pycache__/send_emails.cpython-311.pyc +0 -0
- codenerix_email/management/commands/__pycache__/test_email.cpython-311.pyc +0 -0
- codenerix_email/management/commands/email_test.py +0 -10
- codenerix_email/management/commands/emails_recv.py +0 -10
- codenerix_email/management/commands/emails_send.py +0 -10
- codenerix_email/management/commands/recv_emails.py +1 -601
- codenerix_email/management/commands/send_emails.py +2 -239
- codenerix_email/management/commands/test_email.py +1 -174
- {django_codenerix_email-4.0.35.dist-info → django_codenerix_email-4.0.36.dist-info}/METADATA +1 -1
- {django_codenerix_email-4.0.35.dist-info → django_codenerix_email-4.0.36.dist-info}/RECORD +22 -19
- {django_codenerix_email-4.0.35.dist-info → django_codenerix_email-4.0.36.dist-info}/LICENSE +0 -0
- {django_codenerix_email-4.0.35.dist-info → django_codenerix_email-4.0.36.dist-info}/WHEEL +0 -0
- {django_codenerix_email-4.0.35.dist-info → django_codenerix_email-4.0.36.dist-info}/top_level.txt +0 -0
codenerix_email/__init__.py
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
# See the License for the specific language governing permissions and
|
|
19
19
|
# limitations under the License.
|
|
20
20
|
|
|
21
|
-
import sys
|
|
22
21
|
import json
|
|
23
22
|
|
|
24
23
|
from django.core.management.base import BaseCommand
|
|
@@ -30,15 +29,6 @@ from codenerix_email.models import EmailMessage, EmailTemplate
|
|
|
30
29
|
from codenerix_email import __version__
|
|
31
30
|
from django.core.management import CommandError
|
|
32
31
|
|
|
33
|
-
# Deprecation warning
|
|
34
|
-
if not sys.argv[0].startswith("email"):
|
|
35
|
-
import logging
|
|
36
|
-
|
|
37
|
-
logger = logging.getLogger("codenerix")
|
|
38
|
-
logger.warning(
|
|
39
|
-
"WARNING: 'test_email' is DEPRECATED, switch to 'email_test' instead"
|
|
40
|
-
)
|
|
41
|
-
|
|
42
32
|
|
|
43
33
|
class Command(BaseCommand, Debugger):
|
|
44
34
|
# Show this when the user types help
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import sys
|
|
2
1
|
import re
|
|
3
2
|
|
|
4
3
|
import logging
|
|
@@ -28,15 +27,6 @@ import imaplib # noqa: E402
|
|
|
28
27
|
from imapclient import IMAPClient # noqa: E402
|
|
29
28
|
from imapclient.exceptions import LoginError # noqa: E402
|
|
30
29
|
|
|
31
|
-
# Deprecation warning
|
|
32
|
-
if not sys.argv[0].startswith("email"):
|
|
33
|
-
import logging
|
|
34
|
-
|
|
35
|
-
logger = logging.getLogger("codenerix")
|
|
36
|
-
logger.warning(
|
|
37
|
-
"WARNING: 'recv_emails' is DEPRECATED, switch to 'emails_recv' instead"
|
|
38
|
-
)
|
|
39
|
-
|
|
40
30
|
|
|
41
31
|
class Command(BaseCommand):
|
|
42
32
|
help = "Fetches new emails from the configured IMAP account."
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
# See the License for the specific language governing permissions and
|
|
19
19
|
# limitations under the License.
|
|
20
20
|
|
|
21
|
-
import sys
|
|
22
21
|
import time
|
|
23
22
|
|
|
24
23
|
from django.core.management.base import BaseCommand
|
|
@@ -28,15 +27,6 @@ from django.utils import timezone
|
|
|
28
27
|
from codenerix_lib.debugger import Debugger
|
|
29
28
|
from codenerix_email.models import EmailMessage
|
|
30
29
|
|
|
31
|
-
# Deprecation warning
|
|
32
|
-
if not sys.argv[0].startswith("email"):
|
|
33
|
-
import logging
|
|
34
|
-
|
|
35
|
-
logger = logging.getLogger("codenerix")
|
|
36
|
-
logger.warning(
|
|
37
|
-
"WARNING: 'send_email' is DEPRECATED, switch to 'emails_send' instead"
|
|
38
|
-
)
|
|
39
|
-
|
|
40
30
|
|
|
41
31
|
class Command(BaseCommand, Debugger):
|
|
42
32
|
# Show this when the user types help
|
|
@@ -1,34 +1,5 @@
|
|
|
1
1
|
import sys
|
|
2
|
-
import re
|
|
3
2
|
|
|
4
|
-
import logging
|
|
5
|
-
|
|
6
|
-
from django.conf import settings
|
|
7
|
-
from django.core.management.base import BaseCommand, CommandError
|
|
8
|
-
from zoneinfo import ZoneInfo
|
|
9
|
-
|
|
10
|
-
from email import message_from_bytes
|
|
11
|
-
from email.header import decode_header
|
|
12
|
-
from email.message import Message
|
|
13
|
-
from email.parser import HeaderParser
|
|
14
|
-
from typing import Optional
|
|
15
|
-
|
|
16
|
-
from codenerix_email.models import (
|
|
17
|
-
EmailMessage,
|
|
18
|
-
EmailReceived,
|
|
19
|
-
BOUNCE_SOFT,
|
|
20
|
-
BOUNCE_HARD,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Silence DEBUG logs from imapclient
|
|
25
|
-
logging.getLogger("imapclient").setLevel(logging.WARNING)
|
|
26
|
-
|
|
27
|
-
import imaplib # noqa: E402
|
|
28
|
-
from imapclient import IMAPClient # noqa: E402
|
|
29
|
-
from imapclient.exceptions import LoginError # noqa: E402
|
|
30
|
-
|
|
31
|
-
# Deprecation warning
|
|
32
3
|
if not sys.argv[0].startswith("email"):
|
|
33
4
|
import logging
|
|
34
5
|
|
|
@@ -37,575 +8,4 @@ if not sys.argv[0].startswith("email"):
|
|
|
37
8
|
"WARNING: 'recv_emails' is DEPRECATED, switch to 'emails_recv' instead"
|
|
38
9
|
)
|
|
39
10
|
|
|
40
|
-
|
|
41
|
-
class Command(BaseCommand):
|
|
42
|
-
help = "Fetches new emails from the configured IMAP account."
|
|
43
|
-
|
|
44
|
-
def add_arguments(self, parser):
|
|
45
|
-
# Named (optional) arguments
|
|
46
|
-
parser.add_argument(
|
|
47
|
-
"--silent",
|
|
48
|
-
action="store_true",
|
|
49
|
-
dest="silent",
|
|
50
|
-
default=False,
|
|
51
|
-
help="Enable silent mode",
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
parser.add_argument(
|
|
55
|
-
"--tracking-id", type=str, help="Tracking ID to filter"
|
|
56
|
-
)
|
|
57
|
-
parser.add_argument("--imap-id", type=str, help="IMAP ID to filter")
|
|
58
|
-
parser.add_argument(
|
|
59
|
-
"--message-id", type=str, help="Message-ID to filter"
|
|
60
|
-
)
|
|
61
|
-
parser.add_argument(
|
|
62
|
-
"--all", action="store_true", help="Process all emails"
|
|
63
|
-
)
|
|
64
|
-
parser.add_argument(
|
|
65
|
-
"--rewrite", action="store_true", help="Rewrite existing"
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
def handle(self, *args, **options):
|
|
69
|
-
# Get configuration
|
|
70
|
-
self.silent = options["silent"]
|
|
71
|
-
self.verbose = not self.silent
|
|
72
|
-
self.imap_id = options.get("imap_id")
|
|
73
|
-
self.message_id = options.get("message_id")
|
|
74
|
-
self.tracking_id = options.get("tracking_id")
|
|
75
|
-
self.rewrite = options.get("rewrite", False)
|
|
76
|
-
self.process_all = options.get("all", False)
|
|
77
|
-
|
|
78
|
-
# Show header
|
|
79
|
-
if self.verbose:
|
|
80
|
-
self.stdout.write(
|
|
81
|
-
self.style.SUCCESS("Starting IMAP email synchronization...")
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
# Get configuration from settings
|
|
85
|
-
host = getattr(settings, "IMAP_EMAIL_HOST", None)
|
|
86
|
-
port = getattr(settings, "IMAP_EMAIL_PORT", 993)
|
|
87
|
-
user = getattr(settings, "IMAP_EMAIL_USER", None)
|
|
88
|
-
password = getattr(settings, "IMAP_EMAIL_PASSWORD", None)
|
|
89
|
-
ssl = getattr(settings, "IMAP_EMAIL_SSL", True)
|
|
90
|
-
folder = getattr(settings, "IMAP_EMAIL_INBOX_FOLDER", "INBOX")
|
|
91
|
-
|
|
92
|
-
# Verify that IMAP settings are configured
|
|
93
|
-
if host is not None and port:
|
|
94
|
-
# Validate configuration
|
|
95
|
-
if user is None or password is None:
|
|
96
|
-
if self.silent:
|
|
97
|
-
return
|
|
98
|
-
else:
|
|
99
|
-
raise CommandError(
|
|
100
|
-
"IMAP user or password not configured. Please set "
|
|
101
|
-
"IMAP_EMAIL_USER and IMAP_EMAIL_PASSWORD in settings."
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
try:
|
|
105
|
-
# Connect to the IMAP server
|
|
106
|
-
server = IMAPClient(host, port=port, ssl=ssl)
|
|
107
|
-
except Exception as e:
|
|
108
|
-
raise CommandError(
|
|
109
|
-
f"Failed to connect to IMAP server ("
|
|
110
|
-
f"{host=}, "
|
|
111
|
-
f"{port=}, "
|
|
112
|
-
f"ssl={ssl and 'yes' or 'no'}"
|
|
113
|
-
f"): {e}"
|
|
114
|
-
) from e
|
|
115
|
-
|
|
116
|
-
try:
|
|
117
|
-
# Login and select the inbox
|
|
118
|
-
try:
|
|
119
|
-
server.login(user, password)
|
|
120
|
-
except LoginError as e:
|
|
121
|
-
raise CommandError(
|
|
122
|
-
f"Failed to login to IMAP server with {user=}: {e}"
|
|
123
|
-
) from e
|
|
124
|
-
|
|
125
|
-
if folder:
|
|
126
|
-
try:
|
|
127
|
-
server.select_folder(folder, readonly=False)
|
|
128
|
-
except imaplib.IMAP4.error:
|
|
129
|
-
raise CommandError(f"Failed to select inbox {folder=}")
|
|
130
|
-
|
|
131
|
-
# Process emails
|
|
132
|
-
(created_count, overwritten_count) = self.process(server)
|
|
133
|
-
count = created_count + overwritten_count
|
|
134
|
-
|
|
135
|
-
# Show summary
|
|
136
|
-
if self.verbose:
|
|
137
|
-
self.stdout.write(
|
|
138
|
-
self.style.SUCCESS(
|
|
139
|
-
f"Successfully synchronized {count} emails "
|
|
140
|
-
f"(new: {created_count}, "
|
|
141
|
-
f"overwritten: {overwritten_count})"
|
|
142
|
-
)
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
except Exception as e:
|
|
146
|
-
raise
|
|
147
|
-
self.stderr.write(
|
|
148
|
-
self.style.ERROR(
|
|
149
|
-
f"An error occurred during synchronization: {e}"
|
|
150
|
-
)
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
finally:
|
|
154
|
-
# Logout from the server
|
|
155
|
-
try:
|
|
156
|
-
server.logout()
|
|
157
|
-
except Exception:
|
|
158
|
-
pass
|
|
159
|
-
|
|
160
|
-
elif self.verbose:
|
|
161
|
-
raise CommandError(
|
|
162
|
-
"IMAP settings not configured. Please set IMAP_EMAIL_HOST "
|
|
163
|
-
"and IMAP_EMAIL_PORT in settings."
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
def process(self, server):
|
|
167
|
-
"""
|
|
168
|
-
Connects to the IMAP server and fetches new emails,
|
|
169
|
-
saving them as ReceivedEmail objects.
|
|
170
|
-
"""
|
|
171
|
-
|
|
172
|
-
# Processed emails count
|
|
173
|
-
created_count = 0
|
|
174
|
-
overwrite_count = 0
|
|
175
|
-
|
|
176
|
-
# Look up for emails
|
|
177
|
-
if self.imap_id:
|
|
178
|
-
# Search by specific IMAP ID
|
|
179
|
-
try:
|
|
180
|
-
imap_id = int(self.imap_id)
|
|
181
|
-
except ValueError:
|
|
182
|
-
raise CommandError(
|
|
183
|
-
f"Invalid IMAP ID '{self.imap_id}'. Must be an integer."
|
|
184
|
-
)
|
|
185
|
-
messages_ids = [imap_id]
|
|
186
|
-
if self.verbose:
|
|
187
|
-
self.stdout.write(
|
|
188
|
-
self.style.SUCCESS(
|
|
189
|
-
f"Processing email with IMAP ID {self.imap_id}."
|
|
190
|
-
)
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
elif self.message_id:
|
|
194
|
-
# Search by specific Message-ID
|
|
195
|
-
messages_ids = server.search(
|
|
196
|
-
["HEADER", "Message-ID", self.message_id]
|
|
197
|
-
)
|
|
198
|
-
if self.verbose:
|
|
199
|
-
self.stdout.write(
|
|
200
|
-
self.style.SUCCESS(
|
|
201
|
-
f"Found {len(messages_ids)} email(s) with "
|
|
202
|
-
f"Message-ID {self.message_id}."
|
|
203
|
-
)
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
elif self.process_all:
|
|
207
|
-
# Process all emails
|
|
208
|
-
messages_ids = server.search(["ALL"])
|
|
209
|
-
if self.verbose:
|
|
210
|
-
self.stdout.write(
|
|
211
|
-
self.style.SUCCESS(
|
|
212
|
-
f"Found {len(messages_ids)} email(s) to process."
|
|
213
|
-
)
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
else:
|
|
217
|
-
# Search by UNSEEN
|
|
218
|
-
messages_ids = server.search(["UNSEEN"])
|
|
219
|
-
if self.verbose:
|
|
220
|
-
self.stdout.write(
|
|
221
|
-
self.style.SUCCESS(
|
|
222
|
-
f"Found {len(messages_ids)} new email(s) to process."
|
|
223
|
-
)
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
# If there are new messages, fetch and process them
|
|
227
|
-
if messages_ids:
|
|
228
|
-
# Fetch the full message and internal date
|
|
229
|
-
fetched_data = server.fetch(
|
|
230
|
-
messages_ids, ["BODY.PEEK[]", "INTERNALDATE"]
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
# Get the envelope (metadata) and the full body
|
|
234
|
-
# Use IMAP IDs so identifiers do not change between sessions
|
|
235
|
-
for imap_id, message_data in fetched_data.items():
|
|
236
|
-
# Filter out by IMAP ID if specified
|
|
237
|
-
if self.imap_id and str(imap_id) != self.imap_id:
|
|
238
|
-
continue
|
|
239
|
-
|
|
240
|
-
# Get the raw email and internal date
|
|
241
|
-
raw_email = message_data[b"BODY[]"]
|
|
242
|
-
internal_date_naive = message_data[b"INTERNALDATE"]
|
|
243
|
-
internal_date = internal_date_naive.replace(
|
|
244
|
-
tzinfo=ZoneInfo(settings.TIME_ZONE)
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
# Parse the email
|
|
248
|
-
msg = message_from_bytes(raw_email)
|
|
249
|
-
|
|
250
|
-
# Extract subject, efrom, eto & eid
|
|
251
|
-
subject, encoding = decode_header(msg["Subject"])[0]
|
|
252
|
-
if isinstance(subject, bytes):
|
|
253
|
-
subject = subject.decode(encoding or "utf-8")
|
|
254
|
-
efrom = msg.get("From")
|
|
255
|
-
eto = msg.get("To")
|
|
256
|
-
eid = msg.get("Message-ID")
|
|
257
|
-
|
|
258
|
-
# If we can't get a Message-ID, use the IMAP ID as fallback
|
|
259
|
-
# to avoid duplicates
|
|
260
|
-
if not eid:
|
|
261
|
-
eid = f"<imapid-{imap_id}@{settings.IMAP_EMAIL_HOST}>"
|
|
262
|
-
|
|
263
|
-
# Avoid processing duplicates
|
|
264
|
-
email_received = EmailReceived.objects.filter(eid=eid).first()
|
|
265
|
-
if self.rewrite or not email_received:
|
|
266
|
-
# Process multipart emails
|
|
267
|
-
body_plain = ""
|
|
268
|
-
body_html = ""
|
|
269
|
-
if msg.is_multipart():
|
|
270
|
-
for part in msg.walk():
|
|
271
|
-
content_type = part.get_content_type()
|
|
272
|
-
if content_type == "text/plain" and not body_plain:
|
|
273
|
-
body_plain = part.get_payload(
|
|
274
|
-
decode=True
|
|
275
|
-
).decode(
|
|
276
|
-
part.get_content_charset() or "utf-8",
|
|
277
|
-
"ignore",
|
|
278
|
-
)
|
|
279
|
-
elif content_type == "text/html" and not body_html:
|
|
280
|
-
body_html = part.get_payload(
|
|
281
|
-
decode=True
|
|
282
|
-
).decode(
|
|
283
|
-
part.get_content_charset() or "utf-8",
|
|
284
|
-
"ignore",
|
|
285
|
-
)
|
|
286
|
-
else:
|
|
287
|
-
body_plain = msg.get_payload(decode=True).decode(
|
|
288
|
-
msg.get_content_charset() or "utf-8",
|
|
289
|
-
"ignore",
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
# Logic to associate replies/bounces with sent emails
|
|
293
|
-
try:
|
|
294
|
-
email_message = None
|
|
295
|
-
|
|
296
|
-
# Locate the tracking ID
|
|
297
|
-
tracking_id = self.find_tracking_id(msg)
|
|
298
|
-
|
|
299
|
-
# Filter out by tracking ID if specified
|
|
300
|
-
if (
|
|
301
|
-
self.tracking_id
|
|
302
|
-
and tracking_id != self.tracking_id
|
|
303
|
-
):
|
|
304
|
-
continue
|
|
305
|
-
|
|
306
|
-
# If found, try to link to the sent email
|
|
307
|
-
if tracking_id:
|
|
308
|
-
try:
|
|
309
|
-
email_message = EmailMessage.objects.get(
|
|
310
|
-
uuid=tracking_id
|
|
311
|
-
)
|
|
312
|
-
except EmailMessage.DoesNotExist:
|
|
313
|
-
email_message = None
|
|
314
|
-
if self.verbose:
|
|
315
|
-
self.stdout.write(
|
|
316
|
-
self.style.WARNING(
|
|
317
|
-
f"Tracking ID {tracking_id} found "
|
|
318
|
-
"but no matching sent email."
|
|
319
|
-
)
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
except Exception as e:
|
|
323
|
-
raise CommandError(
|
|
324
|
-
"Error while linking email with IMAP ID "
|
|
325
|
-
f"{imap_id} to sent email: {e}"
|
|
326
|
-
) from e
|
|
327
|
-
|
|
328
|
-
# Heuristic keywords commonly found in bounce messages
|
|
329
|
-
(bounce_type, bounce_reason) = self.analyze_bounce(msg)
|
|
330
|
-
|
|
331
|
-
# Extract all headers into a dictionary
|
|
332
|
-
headers = {}
|
|
333
|
-
for header, value in msg.items():
|
|
334
|
-
decoded_value, encoding = decode_header(value)[0]
|
|
335
|
-
if isinstance(decoded_value, bytes):
|
|
336
|
-
decoded_value = decoded_value.decode(
|
|
337
|
-
encoding or "utf-8", "ignore"
|
|
338
|
-
)
|
|
339
|
-
headers[header] = decoded_value
|
|
340
|
-
|
|
341
|
-
# Create EmailReceived object if doesn't exist
|
|
342
|
-
if not email_received:
|
|
343
|
-
overwriting = False
|
|
344
|
-
email_received = EmailReceived()
|
|
345
|
-
else:
|
|
346
|
-
overwriting = True
|
|
347
|
-
|
|
348
|
-
# Populate fields
|
|
349
|
-
email_received.imap_id = imap_id
|
|
350
|
-
email_received.eid = eid
|
|
351
|
-
email_received.efrom = efrom
|
|
352
|
-
email_received.eto = eto
|
|
353
|
-
email_received.subject = subject
|
|
354
|
-
email_received.headers = headers
|
|
355
|
-
email_received.body_text = body_plain
|
|
356
|
-
email_received.body_html = body_html
|
|
357
|
-
email_received.date_received = internal_date
|
|
358
|
-
email_received.email = email_message
|
|
359
|
-
email_received.bounce_type = bounce_type
|
|
360
|
-
email_received.bounce_reason = bounce_reason
|
|
361
|
-
|
|
362
|
-
# Save the received email
|
|
363
|
-
email_received.save()
|
|
364
|
-
|
|
365
|
-
# Count created or overwritten
|
|
366
|
-
if overwriting:
|
|
367
|
-
overwrite_count += 1
|
|
368
|
-
verb = "Overwritten"
|
|
369
|
-
else:
|
|
370
|
-
created_count += 1
|
|
371
|
-
verb = "Created"
|
|
372
|
-
|
|
373
|
-
if self.verbose:
|
|
374
|
-
msg = (
|
|
375
|
-
f"{verb} email with IMAP ID: "
|
|
376
|
-
f"{imap_id} (link={tracking_id})"
|
|
377
|
-
)
|
|
378
|
-
if bounce_type:
|
|
379
|
-
bounce_type_str = (
|
|
380
|
-
bounce_type == BOUNCE_HARD and "Hard" or "Soft"
|
|
381
|
-
)
|
|
382
|
-
bounce_reason_str = bounce_reason or "Unknown"
|
|
383
|
-
self.stdout.write(
|
|
384
|
-
self.style.WARNING(
|
|
385
|
-
f"{msg} "
|
|
386
|
-
f"[{bounce_type_str} bounce, "
|
|
387
|
-
f"reason={bounce_reason_str}]"
|
|
388
|
-
)
|
|
389
|
-
)
|
|
390
|
-
else:
|
|
391
|
-
self.stdout.write(self.style.SUCCESS(msg))
|
|
392
|
-
|
|
393
|
-
else:
|
|
394
|
-
if self.verbose:
|
|
395
|
-
self.stdout.write(
|
|
396
|
-
self.style.WARNING(
|
|
397
|
-
f"Skipping email with IMAP ID: {imap_id} (DUP)"
|
|
398
|
-
)
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
# Mark the message as read
|
|
402
|
-
# (flag \Seen) avoid reprocessing
|
|
403
|
-
server.add_flags(imap_id, [b"\\Seen"])
|
|
404
|
-
|
|
405
|
-
return (created_count, overwrite_count)
|
|
406
|
-
|
|
407
|
-
def find_tracking_id(self, msg: Message) -> str | None:
|
|
408
|
-
"""
|
|
409
|
-
Searches for the X-Codenerix-Tracking-ID robustly in an email.
|
|
410
|
-
|
|
411
|
-
It performs the search in three steps:
|
|
412
|
-
1. In the main headers of the email.
|
|
413
|
-
2. In the attached parts that are a complete email (message/rfc822).
|
|
414
|
-
3. As a last resort, searches the text in the body of the message.
|
|
415
|
-
"""
|
|
416
|
-
|
|
417
|
-
# Method 1: Search in main headers (for direct replies)
|
|
418
|
-
tracking_id = msg.get("X-Codenerix-Tracking-ID", None)
|
|
419
|
-
|
|
420
|
-
# Method 2: Search in attached parts (for bounces and forwards)
|
|
421
|
-
if not tracking_id:
|
|
422
|
-
# Not found directly in headers
|
|
423
|
-
# Search in attached parts (for bounces and forwards)
|
|
424
|
-
if msg.is_multipart():
|
|
425
|
-
# Iterate through parts
|
|
426
|
-
for part in msg.walk():
|
|
427
|
-
# Get the content type of the part
|
|
428
|
-
content_type = part.get_content_type()
|
|
429
|
-
|
|
430
|
-
# We look for an attachment that is itself an email
|
|
431
|
-
if content_type == "message/rfc822":
|
|
432
|
-
# The payload of this part is the original email
|
|
433
|
-
# The payload is a list of messages, take the first one
|
|
434
|
-
original_msg_payload = part.get_payload()
|
|
435
|
-
if (
|
|
436
|
-
isinstance(original_msg_payload, list)
|
|
437
|
-
and original_msg_payload
|
|
438
|
-
):
|
|
439
|
-
original_msg = original_msg_payload[0]
|
|
440
|
-
if isinstance(original_msg, Message):
|
|
441
|
-
tracking_id = original_msg.get(
|
|
442
|
-
"X-Codenerix-Tracking-ID"
|
|
443
|
-
)
|
|
444
|
-
|
|
445
|
-
elif content_type == "text/rfc822-headers":
|
|
446
|
-
# The payload is the raw headers of the original email
|
|
447
|
-
headers_payload = part.get_payload(decode=True)
|
|
448
|
-
if isinstance(headers_payload, bytes):
|
|
449
|
-
# Decode using the specified charset
|
|
450
|
-
charset = part.get_content_charset() or "utf-8"
|
|
451
|
-
headers_text = headers_payload.decode(
|
|
452
|
-
charset, errors="ignore"
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
# Parse headers text into a Message object
|
|
456
|
-
headers_msg = HeaderParser().parsestr(headers_text)
|
|
457
|
-
tracking_id = headers_msg.get(
|
|
458
|
-
"X-Codenerix-Tracking-ID"
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
# Method 3: Search in the body text (fallback)
|
|
462
|
-
if not tracking_id:
|
|
463
|
-
# The original email might be quoted as plain text.
|
|
464
|
-
body_text = ""
|
|
465
|
-
if msg.is_multipart():
|
|
466
|
-
# Concatenate all text/plain parts
|
|
467
|
-
for part in msg.walk():
|
|
468
|
-
# We only want text/plain parts
|
|
469
|
-
if part.get_content_type() == "text/plain":
|
|
470
|
-
# Get the decoded payload
|
|
471
|
-
payload = part.get_payload(decode=True)
|
|
472
|
-
if isinstance(payload, bytes):
|
|
473
|
-
# Decode using the specified charset
|
|
474
|
-
charset = part.get_content_charset() or "utf-8"
|
|
475
|
-
body_text += payload.decode(
|
|
476
|
-
charset, errors="ignore"
|
|
477
|
-
)
|
|
478
|
-
else:
|
|
479
|
-
# Single part email, check if it's text/plain
|
|
480
|
-
if msg.get_content_type() == "text/plain":
|
|
481
|
-
# Get the decoded payload
|
|
482
|
-
payload = msg.get_payload(decode=True)
|
|
483
|
-
if isinstance(payload, bytes):
|
|
484
|
-
# Decode using the specified charset
|
|
485
|
-
charset = msg.get_content_charset() or "utf-8"
|
|
486
|
-
body_text = payload.decode(
|
|
487
|
-
charset, errors="ignore"
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
# If we have body text, search for the header using regex
|
|
491
|
-
if body_text:
|
|
492
|
-
# We use a regex to find the header in the text
|
|
493
|
-
match = re.search(
|
|
494
|
-
r"X-Codenerix-Tracking-ID:\s*([a-fA-F0-9\-]{36})",
|
|
495
|
-
body_text,
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
if match:
|
|
499
|
-
# If found, extract the tracking ID
|
|
500
|
-
tracking_id = match.group(1).strip()
|
|
501
|
-
|
|
502
|
-
# Return the found tracking ID if any
|
|
503
|
-
return tracking_id
|
|
504
|
-
|
|
505
|
-
def analyze_bounce(
|
|
506
|
-
self, msg: Message
|
|
507
|
-
) -> tuple[Optional[str], Optional[str]]:
|
|
508
|
-
"""
|
|
509
|
-
Analyzes an email to determine if it is a bounce and of what type.
|
|
510
|
-
|
|
511
|
-
Returns:
|
|
512
|
-
A tuple (bounce_type, smtp_code).
|
|
513
|
-
- bounce_type: BOUNCE_HARD, BOUNCE_SOFT, or None if not a bounce.
|
|
514
|
-
- bounce_reason: the SMTP status code (e.g., '5.1.1') or None.
|
|
515
|
-
"""
|
|
516
|
-
|
|
517
|
-
# Initialize
|
|
518
|
-
bounce_type: Optional[str] = None
|
|
519
|
-
bounce_reason: Optional[str] = None
|
|
520
|
-
|
|
521
|
-
# Method 1: Look for DSN reports
|
|
522
|
-
if (
|
|
523
|
-
msg.get_content_type() == "multipart/report"
|
|
524
|
-
and msg.get_param("report-type") == "delivery-status"
|
|
525
|
-
):
|
|
526
|
-
# Iterate through parts to find the delivery-status part
|
|
527
|
-
for part in msg.walk():
|
|
528
|
-
# We look for the delivery-status part
|
|
529
|
-
if part.get_content_type() == "message/delivery-status":
|
|
530
|
-
# The payload is a list of headers
|
|
531
|
-
payload = part.get_payload()
|
|
532
|
-
if payload and isinstance(payload, list):
|
|
533
|
-
# The first part contains the status headers
|
|
534
|
-
status_headers = payload[0]
|
|
535
|
-
if isinstance(status_headers, Message):
|
|
536
|
-
# Extract Action and Status headers
|
|
537
|
-
action = status_headers.get("Action", "").lower()
|
|
538
|
-
status_code = status_headers.get("Status", "")
|
|
539
|
-
|
|
540
|
-
# Check if action indicates failure
|
|
541
|
-
if action == "failed":
|
|
542
|
-
# Determine Hard/Soft by SMTP code (RFC3463)
|
|
543
|
-
if status_code.startswith("5."):
|
|
544
|
-
# 5.x.x: permanent failure (hard)
|
|
545
|
-
bounce_type = BOUNCE_HARD
|
|
546
|
-
bounce_reason = status_code
|
|
547
|
-
break
|
|
548
|
-
elif status_code.startswith("4."):
|
|
549
|
-
# 4.x.x: temporary failure (soft)
|
|
550
|
-
bounce_type = BOUNCE_SOFT
|
|
551
|
-
bounce_reason = status_code
|
|
552
|
-
break
|
|
553
|
-
else:
|
|
554
|
-
# Unknown status, assume hard bounce
|
|
555
|
-
bounce_type = BOUNCE_HARD
|
|
556
|
-
bounce_reason = status_code or "Unknown"
|
|
557
|
-
break
|
|
558
|
-
|
|
559
|
-
# Method 2: Some mail servers include headers indicating a bounce
|
|
560
|
-
if not bounce_type:
|
|
561
|
-
if msg.get("X-Failed-Recipients"):
|
|
562
|
-
# Presence of this header usually indicates a hard bounce
|
|
563
|
-
bounce_type = BOUNCE_HARD
|
|
564
|
-
bounce_reason = "Unknown (X-Failed-Recipients)"
|
|
565
|
-
|
|
566
|
-
else:
|
|
567
|
-
# Check for Auto-Submitted header
|
|
568
|
-
if msg.get("Auto-Submitted", "").lower() in (
|
|
569
|
-
"auto-replied",
|
|
570
|
-
"auto-generated",
|
|
571
|
-
):
|
|
572
|
-
# It could be a bounce, but also an "Out of Office",
|
|
573
|
-
# so we combine it with a keyword search.
|
|
574
|
-
subject = msg.get("Subject", "").lower()
|
|
575
|
-
bounce_keywords = [
|
|
576
|
-
"undeliverable",
|
|
577
|
-
"delivery failed",
|
|
578
|
-
"failure notice",
|
|
579
|
-
]
|
|
580
|
-
|
|
581
|
-
# If we find bounce keywords in the subject
|
|
582
|
-
if any(keyword in subject for keyword in bounce_keywords):
|
|
583
|
-
# Assume is a hard bounce
|
|
584
|
-
bounce_type = BOUNCE_HARD
|
|
585
|
-
bounce_reason = "Unknown (Auto-Submitted + Keyword)"
|
|
586
|
-
|
|
587
|
-
# Method 3: keyword search (less reliable)
|
|
588
|
-
if not bounce_type:
|
|
589
|
-
# We look for common bounce keywords in the From or Subject headers
|
|
590
|
-
# We avoid false positives by requiring specific keywords.
|
|
591
|
-
from_header = msg.get("From", "").lower()
|
|
592
|
-
subject_header = msg.get("Subject", "").lower()
|
|
593
|
-
|
|
594
|
-
if "mailer-daemon@" in from_header or "postmaster@" in from_header:
|
|
595
|
-
# Common bounce sender addresses
|
|
596
|
-
bounce_type = BOUNCE_HARD
|
|
597
|
-
bounce_reason = "Unknown (From Keyword)"
|
|
598
|
-
else:
|
|
599
|
-
# Check subject for common bounce keywords
|
|
600
|
-
bounce_keywords = [
|
|
601
|
-
"undelivered",
|
|
602
|
-
"delivery error",
|
|
603
|
-
"mail delivery failed",
|
|
604
|
-
]
|
|
605
|
-
if any(
|
|
606
|
-
keyword in subject_header for keyword in bounce_keywords
|
|
607
|
-
):
|
|
608
|
-
bounce_type = BOUNCE_HARD
|
|
609
|
-
bounce_reason = "Unknown (Subject Keyword)"
|
|
610
|
-
|
|
611
|
-
return (bounce_type, bounce_reason)
|
|
11
|
+
from .emails_recv import * # type: ignore # noqa
|
|
@@ -1,248 +1,11 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
#
|
|
3
|
-
# django-codenerix-email
|
|
4
|
-
#
|
|
5
|
-
# Codenerix GNU
|
|
6
|
-
#
|
|
7
|
-
# Project URL : http://www.codenerix.com
|
|
8
|
-
#
|
|
9
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
-
# you may not use this file except in compliance with the License.
|
|
11
|
-
# You may obtain a copy of the License at
|
|
12
|
-
#
|
|
13
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
14
|
-
#
|
|
15
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
16
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
17
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
18
|
-
# See the License for the specific language governing permissions and
|
|
19
|
-
# limitations under the License.
|
|
20
|
-
|
|
21
1
|
import sys
|
|
22
|
-
import time
|
|
23
|
-
|
|
24
|
-
from django.core.management.base import BaseCommand
|
|
25
|
-
from django.conf import settings
|
|
26
|
-
from django.utils import timezone
|
|
27
2
|
|
|
28
|
-
from codenerix_lib.debugger import Debugger
|
|
29
|
-
from codenerix_email.models import EmailMessage
|
|
30
|
-
|
|
31
|
-
# Deprecation warning
|
|
32
3
|
if not sys.argv[0].startswith("email"):
|
|
33
4
|
import logging
|
|
34
5
|
|
|
35
6
|
logger = logging.getLogger("codenerix")
|
|
36
7
|
logger.warning(
|
|
37
|
-
"WARNING: '
|
|
8
|
+
"WARNING: 'send_emails' is DEPRECATED, switch to 'emails_send' instead"
|
|
38
9
|
)
|
|
39
10
|
|
|
40
|
-
|
|
41
|
-
class Command(BaseCommand, Debugger):
|
|
42
|
-
# Show this when the user types help
|
|
43
|
-
help = "Try to send all emails in the queue"
|
|
44
|
-
|
|
45
|
-
def add_arguments(self, parser):
|
|
46
|
-
# Named (optional) arguments
|
|
47
|
-
parser.add_argument(
|
|
48
|
-
"-d",
|
|
49
|
-
action="store_true",
|
|
50
|
-
dest="d",
|
|
51
|
-
default=False,
|
|
52
|
-
help="Keep the command working forever as a daemon",
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
# Named (optional) arguments
|
|
56
|
-
parser.add_argument(
|
|
57
|
-
"--daemon",
|
|
58
|
-
action="store_true",
|
|
59
|
-
dest="daemon",
|
|
60
|
-
default=False,
|
|
61
|
-
help="Keep the command working forever as a daemon",
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
# Named (optional) arguments
|
|
65
|
-
parser.add_argument(
|
|
66
|
-
"-c",
|
|
67
|
-
action="store_true",
|
|
68
|
-
dest="c",
|
|
69
|
-
default=False,
|
|
70
|
-
help="Clear the sending status to all the Queue",
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
# Named (optional) arguments
|
|
74
|
-
parser.add_argument(
|
|
75
|
-
"--clear",
|
|
76
|
-
action="store_true",
|
|
77
|
-
dest="clear",
|
|
78
|
-
default=False,
|
|
79
|
-
help="Clear the sending status to all the Queue",
|
|
80
|
-
)
|
|
81
|
-
# Named (optional) arguments
|
|
82
|
-
parser.add_argument(
|
|
83
|
-
"--verbose",
|
|
84
|
-
action="store_true",
|
|
85
|
-
dest="verbose",
|
|
86
|
-
default=False,
|
|
87
|
-
help="Enable verbose mode",
|
|
88
|
-
)
|
|
89
|
-
# Named (optional) arguments
|
|
90
|
-
parser.add_argument(
|
|
91
|
-
"--now",
|
|
92
|
-
action="store_true",
|
|
93
|
-
dest="now",
|
|
94
|
-
default=False,
|
|
95
|
-
help="Send now, do not wait the retry time",
|
|
96
|
-
)
|
|
97
|
-
# Named (optional) arguments
|
|
98
|
-
parser.add_argument(
|
|
99
|
-
"--all",
|
|
100
|
-
action="store_true",
|
|
101
|
-
dest="all",
|
|
102
|
-
default=False,
|
|
103
|
-
help="Send all, do not do on buckets",
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
def handle(self, *args, **options):
|
|
107
|
-
# Get user configuration
|
|
108
|
-
daemon = bool(options["daemon"] or options["d"])
|
|
109
|
-
clear = bool(options["clear"] or options["c"])
|
|
110
|
-
bucket_size = getattr(settings, "CLIENT_EMAIL_BUCKETS", 10)
|
|
111
|
-
verbose = bool(options.get("verbose", False))
|
|
112
|
-
sendnow = bool(options.get("now", False))
|
|
113
|
-
doall = bool(options.get("all", False))
|
|
114
|
-
|
|
115
|
-
# Autoconfigure Debugger
|
|
116
|
-
self.set_name("CODENERIX-EMAIL")
|
|
117
|
-
self.set_debug()
|
|
118
|
-
|
|
119
|
-
# Daemon
|
|
120
|
-
if verbose:
|
|
121
|
-
if daemon:
|
|
122
|
-
self.debug(
|
|
123
|
-
"Starting command in DAEMON mode with a "
|
|
124
|
-
f"queue of {bucket_size} emails",
|
|
125
|
-
color="cyan",
|
|
126
|
-
)
|
|
127
|
-
else:
|
|
128
|
-
self.debug(
|
|
129
|
-
"Starting a queue of {} emails".format(bucket_size),
|
|
130
|
-
color="blue",
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# In if requested set sending status for all the list to False
|
|
134
|
-
if clear:
|
|
135
|
-
EmailMessage.objects.filter(sending=True).update(sending=False)
|
|
136
|
-
|
|
137
|
-
# Get a bunch of emails in the queue
|
|
138
|
-
connection = None
|
|
139
|
-
|
|
140
|
-
# If daemon mode is requested
|
|
141
|
-
first = True
|
|
142
|
-
while first or daemon:
|
|
143
|
-
# Get a bucket of emails
|
|
144
|
-
emails = EmailMessage.objects.filter(
|
|
145
|
-
sent=False,
|
|
146
|
-
sending=False,
|
|
147
|
-
error=False,
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
# If we do not have to send now we have to wait for the next retry
|
|
151
|
-
if not sendnow:
|
|
152
|
-
emails = emails.filter(
|
|
153
|
-
next_retry__lte=timezone.now(),
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
# Order emails by priority and next retry
|
|
157
|
-
emails = emails.order_by("priority", "next_retry")
|
|
158
|
-
|
|
159
|
-
# Send in buckets if we are not doing them all
|
|
160
|
-
if not doall:
|
|
161
|
-
emails = emails[0 : bucket_size + 1]
|
|
162
|
-
|
|
163
|
-
# Check if there are emails to process
|
|
164
|
-
if emails:
|
|
165
|
-
# Convert to list
|
|
166
|
-
list_emails = [x.pk for x in emails]
|
|
167
|
-
|
|
168
|
-
# Set sending status for all the list
|
|
169
|
-
EmailMessage.objects.filter(pk__in=list_emails).update(
|
|
170
|
-
sending=True
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
# For each email
|
|
174
|
-
for email in emails:
|
|
175
|
-
if verbose:
|
|
176
|
-
self.debug(
|
|
177
|
-
"Sending to {}".format(email.eto),
|
|
178
|
-
color="white",
|
|
179
|
-
tail=False,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
# Check if we have connection
|
|
183
|
-
if not connection:
|
|
184
|
-
if verbose:
|
|
185
|
-
self.debug(
|
|
186
|
-
" - Connecting",
|
|
187
|
-
color="yellow",
|
|
188
|
-
head=False,
|
|
189
|
-
tail=False,
|
|
190
|
-
)
|
|
191
|
-
connection = email.connect()
|
|
192
|
-
|
|
193
|
-
# Send the email
|
|
194
|
-
try:
|
|
195
|
-
email.send(connection, debug=False)
|
|
196
|
-
except Exception as e:
|
|
197
|
-
email.sending = False
|
|
198
|
-
error = "Exception: {}\n".format(e)
|
|
199
|
-
if email.log:
|
|
200
|
-
email.log += error
|
|
201
|
-
else:
|
|
202
|
-
email.log = error
|
|
203
|
-
email.save()
|
|
204
|
-
self.error(error)
|
|
205
|
-
if verbose:
|
|
206
|
-
if email.sent:
|
|
207
|
-
self.debug(" -> SENT", color="green", head=False)
|
|
208
|
-
else:
|
|
209
|
-
self.debug(
|
|
210
|
-
" -> ERROR",
|
|
211
|
-
color="red",
|
|
212
|
-
head=False,
|
|
213
|
-
tail=False,
|
|
214
|
-
)
|
|
215
|
-
self.debug(
|
|
216
|
-
" ({} retries left)".format(
|
|
217
|
-
getattr(
|
|
218
|
-
settings, "CLIENT_EMAIL_RETRIES", 10
|
|
219
|
-
)
|
|
220
|
-
- email.retries
|
|
221
|
-
),
|
|
222
|
-
color="cyan",
|
|
223
|
-
head=False,
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
# Delete all that have been sent
|
|
227
|
-
if not getattr(settings, "CLIENT_EMAIL_HISTORY", True):
|
|
228
|
-
EmailMessage.objects.filter(
|
|
229
|
-
pk__in=list_emails, sent=True
|
|
230
|
-
).delete()
|
|
231
|
-
|
|
232
|
-
elif daemon:
|
|
233
|
-
# Sleep for a while
|
|
234
|
-
try:
|
|
235
|
-
time.sleep(10)
|
|
236
|
-
except KeyboardInterrupt:
|
|
237
|
-
self.debug("Exited by user request!", color="green")
|
|
238
|
-
break
|
|
239
|
-
|
|
240
|
-
elif verbose:
|
|
241
|
-
# No emails to send
|
|
242
|
-
self.debug(
|
|
243
|
-
"No emails to be sent at this moment in the queue!",
|
|
244
|
-
color="green",
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
# This was the first time
|
|
248
|
-
first = False
|
|
11
|
+
from .emails_send import * # type: ignore # noqa
|
|
@@ -1,36 +1,5 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
#
|
|
3
|
-
# django-codenerix-email
|
|
4
|
-
#
|
|
5
|
-
# Codenerix GNU
|
|
6
|
-
#
|
|
7
|
-
# Project URL : http://www.codenerix.com
|
|
8
|
-
#
|
|
9
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
-
# you may not use this file except in compliance with the License.
|
|
11
|
-
# You may obtain a copy of the License at
|
|
12
|
-
#
|
|
13
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
14
|
-
#
|
|
15
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
16
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
17
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
18
|
-
# See the License for the specific language governing permissions and
|
|
19
|
-
# limitations under the License.
|
|
20
|
-
|
|
21
1
|
import sys
|
|
22
|
-
import json
|
|
23
|
-
|
|
24
|
-
from django.core.management.base import BaseCommand
|
|
25
|
-
from django.conf import settings
|
|
26
|
-
from django.utils import timezone
|
|
27
|
-
|
|
28
|
-
from codenerix_lib.debugger import Debugger
|
|
29
|
-
from codenerix_email.models import EmailMessage, EmailTemplate
|
|
30
|
-
from codenerix_email import __version__
|
|
31
|
-
from django.core.management import CommandError
|
|
32
2
|
|
|
33
|
-
# Deprecation warning
|
|
34
3
|
if not sys.argv[0].startswith("email"):
|
|
35
4
|
import logging
|
|
36
5
|
|
|
@@ -39,146 +8,4 @@ if not sys.argv[0].startswith("email"):
|
|
|
39
8
|
"WARNING: 'test_email' is DEPRECATED, switch to 'email_test' instead"
|
|
40
9
|
)
|
|
41
10
|
|
|
42
|
-
|
|
43
|
-
class Command(BaseCommand, Debugger):
|
|
44
|
-
# Show this when the user types help
|
|
45
|
-
help = "Test"
|
|
46
|
-
|
|
47
|
-
def add_arguments(self, parser):
|
|
48
|
-
parser.add_argument(
|
|
49
|
-
"--email",
|
|
50
|
-
type=str,
|
|
51
|
-
help="Email",
|
|
52
|
-
default=None,
|
|
53
|
-
)
|
|
54
|
-
parser.add_argument(
|
|
55
|
-
"--template",
|
|
56
|
-
type=str,
|
|
57
|
-
help="Template CID",
|
|
58
|
-
default=None,
|
|
59
|
-
)
|
|
60
|
-
parser.add_argument(
|
|
61
|
-
"--context",
|
|
62
|
-
type=str,
|
|
63
|
-
help="Context as JSON",
|
|
64
|
-
default="{}",
|
|
65
|
-
)
|
|
66
|
-
parser.add_argument(
|
|
67
|
-
"--language",
|
|
68
|
-
type=str,
|
|
69
|
-
help="Language",
|
|
70
|
-
default=None,
|
|
71
|
-
)
|
|
72
|
-
parser.add_argument(
|
|
73
|
-
"--stdout",
|
|
74
|
-
action="store_true",
|
|
75
|
-
help="Print to stdout",
|
|
76
|
-
default=False,
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
def handle(self, *args, **options):
|
|
80
|
-
# Autoconfigure Debugger
|
|
81
|
-
self.set_name("CODENERIX-EMAIL")
|
|
82
|
-
self.set_debug()
|
|
83
|
-
|
|
84
|
-
# Get arguments
|
|
85
|
-
email = options["email"]
|
|
86
|
-
template = options["template"]
|
|
87
|
-
context_str = options["context"]
|
|
88
|
-
language = options["language"]
|
|
89
|
-
stdout = options["stdout"]
|
|
90
|
-
|
|
91
|
-
# Read context
|
|
92
|
-
try:
|
|
93
|
-
context = json.loads(context_str)
|
|
94
|
-
except json.JSONDecodeError:
|
|
95
|
-
raise CommandError(
|
|
96
|
-
"Context is not a valid JSON string: {}".format(context_str)
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# If no template is provided, use the default one
|
|
100
|
-
if template is None:
|
|
101
|
-
# Get the default template
|
|
102
|
-
message = """Hello,
|
|
103
|
-
|
|
104
|
-
This email has been sent using Django Codenerix Email.
|
|
105
|
-
|
|
106
|
-
Best regards, Codenerix Team
|
|
107
|
-
|
|
108
|
-
--
|
|
109
|
-
Django Codenerix Email v{}
|
|
110
|
-
""".format(
|
|
111
|
-
__version__
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
def email_message_factory(context, language):
|
|
115
|
-
email_message = EmailMessage()
|
|
116
|
-
email_message.subject = "[Codenerix Email] Test"
|
|
117
|
-
email_message.body = message
|
|
118
|
-
return email_message
|
|
119
|
-
|
|
120
|
-
else:
|
|
121
|
-
# Get the template
|
|
122
|
-
try:
|
|
123
|
-
template = EmailTemplate.objects.get(cid=template)
|
|
124
|
-
except EmailTemplate.DoesNotExist:
|
|
125
|
-
raise CommandError(
|
|
126
|
-
"Template with CID {} does not exist.".format(template)
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
# Render the template
|
|
130
|
-
def email_message_factory(context, language):
|
|
131
|
-
return template.get_email(context, language)
|
|
132
|
-
|
|
133
|
-
# If no email is provided, send to all admins
|
|
134
|
-
if email is None:
|
|
135
|
-
# Send email to all admins
|
|
136
|
-
for name, email in settings.ADMINS:
|
|
137
|
-
email_message = email_message_factory(context, language)
|
|
138
|
-
email_message.efrom = settings.DEFAULT_FROM_EMAIL
|
|
139
|
-
email_message.eto = email
|
|
140
|
-
|
|
141
|
-
# Prepare message ID info
|
|
142
|
-
ecid = email_message.uuid.hex
|
|
143
|
-
edomain = settings.EMAIL_FROM.split("@")[-1]
|
|
144
|
-
ets = int(timezone.now().timestamp())
|
|
145
|
-
email_message.headers = {
|
|
146
|
-
"Message-ID": f"<{ecid}-{ets}@{edomain}>",
|
|
147
|
-
"X-Codenerix-Email": "Test",
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if stdout:
|
|
151
|
-
self.debug(
|
|
152
|
-
f"Sending email to {name} <{email}> "
|
|
153
|
-
f"with subject: {email_message.subject}:\n"
|
|
154
|
-
f"{email_message.body}",
|
|
155
|
-
color="white",
|
|
156
|
-
)
|
|
157
|
-
else:
|
|
158
|
-
email_message.save()
|
|
159
|
-
email_message.send(legacy=False, silent=False)
|
|
160
|
-
else:
|
|
161
|
-
# Send email to the specified address
|
|
162
|
-
email_message = email_message_factory(context, language)
|
|
163
|
-
email_message.efrom = settings.DEFAULT_FROM_EMAIL
|
|
164
|
-
email_message.eto = email
|
|
165
|
-
|
|
166
|
-
# Prepare message ID info
|
|
167
|
-
ecid = email_message.uuid.hex
|
|
168
|
-
edomain = settings.EMAIL_FROM.split("@")[-1]
|
|
169
|
-
ets = int(timezone.now().timestamp())
|
|
170
|
-
email_message.headers = {
|
|
171
|
-
"Message-ID": f"<{ecid}-{ets}@{edomain}>",
|
|
172
|
-
"X-Codenerix-Email": "Test",
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if stdout:
|
|
176
|
-
self.debug(
|
|
177
|
-
f"Sending email to <{email}> "
|
|
178
|
-
f"with subject: {email_message.subject}:\n"
|
|
179
|
-
f"{email_message.body}",
|
|
180
|
-
color="white",
|
|
181
|
-
)
|
|
182
|
-
else:
|
|
183
|
-
email_message.save()
|
|
184
|
-
email_message.send(legacy=False, silent=False)
|
|
11
|
+
from .email_test import * # type: ignore # noqa
|
{django_codenerix_email-4.0.35.dist-info → django_codenerix_email-4.0.36.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-codenerix-email
|
|
3
|
-
Version: 4.0.
|
|
3
|
+
Version: 4.0.36
|
|
4
4
|
Summary: Codenerix Email is a module that enables CODENERIX to set send emails in a general manner.
|
|
5
5
|
Home-page: https://github.com/codenerix/django-codenerix-email
|
|
6
6
|
Author: Juan Miguel Taboada Godoy <juanmi@juanmitaboada.com>, Juan Soler Ruiz <soleronline@gmail.com>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
codenerix_email/__init__.py,sha256=
|
|
1
|
+
codenerix_email/__init__.py,sha256=jSnbkwb7yLhUUQ_5XYdtPahUTqy4qyDhY4I89pNo1P0,149
|
|
2
2
|
codenerix_email/admin.py,sha256=w259UKFk_opGEl6PJjYHXWAHQ_8emgqmiixKT5Rid4A,1180
|
|
3
3
|
codenerix_email/apps.py,sha256=WXqu1XQibDDyCvvQYt2JbTK4GIpW8BNv5DCbRJS2mmk,149
|
|
4
4
|
codenerix_email/forms.py,sha256=38byLGxg1MOLAY1kAYChxYZj64tSgyfRvDcSbIOdV0I,5521
|
|
@@ -827,8 +827,8 @@ codenerix_email/.mypy_cache/3.10/zoneinfo/_common.data.json,sha256=e4xbNKL_yQ5h5
|
|
|
827
827
|
codenerix_email/.mypy_cache/3.10/zoneinfo/_common.meta.json,sha256=5K19XWobpSjKwiv7bZFZRcxsEfoh7b-FhYcmqB-2Iic,1737
|
|
828
828
|
codenerix_email/.mypy_cache/3.10/zoneinfo/_tzpath.data.json,sha256=CFx7Q1XfUhhuNX69prkxyirG8rfvEDCNgEHWQigKC_A,5632
|
|
829
829
|
codenerix_email/.mypy_cache/3.10/zoneinfo/_tzpath.meta.json,sha256=d1HJ_xFBI1orlZSVhH0gHWLI-dJG3zY-ZOlctOl62yU,1765
|
|
830
|
-
codenerix_email/__pycache__/__init__.cpython-310.pyc,sha256=
|
|
831
|
-
codenerix_email/__pycache__/__init__.cpython-311.pyc,sha256=
|
|
830
|
+
codenerix_email/__pycache__/__init__.cpython-310.pyc,sha256=_4f3J5XdU0j52LgeGDjFTjpk0xy205v8xQ-OXamh4Uw,313
|
|
831
|
+
codenerix_email/__pycache__/__init__.cpython-311.pyc,sha256=VJN7jdoI4Q1zVQ_yQG9k1SqUHuDaT8RrsDbwJD1-iaI,337
|
|
832
832
|
codenerix_email/__pycache__/__init__.cpython-35.pyc,sha256=dl9lYAgrokJptUj3JAhiqTlX7d_CbncOxZeTc1USc88,308
|
|
833
833
|
codenerix_email/__pycache__/__init__.cpython-37.pyc,sha256=5d1CeFU5DrfnwrRpvSw1bHvLN9hoHXjUA3ln3rXCDo8,306
|
|
834
834
|
codenerix_email/__pycache__/__init__.cpython-39.pyc,sha256=0c6KWU_eOTlF5l9fNWv8l41b0LcfVQNUsqDvJTv2YyU,300
|
|
@@ -843,8 +843,8 @@ codenerix_email/__pycache__/forms.cpython-310.pyc,sha256=-oq5NlQGH7VkMUUMB4lXmBo
|
|
|
843
843
|
codenerix_email/__pycache__/forms.cpython-311.pyc,sha256=zNvq_Xc_bdUsG59tq9OWUiy5hoyZ035z4E5fEovr4NE,5854
|
|
844
844
|
codenerix_email/__pycache__/forms.cpython-35.pyc,sha256=z1aCJ2d8yyKJMuf5aVz0mGE6Nqi9bjU7HyDZPKxpUWc,3233
|
|
845
845
|
codenerix_email/__pycache__/forms.cpython-39.pyc,sha256=NORLA0i3bmWx-mUn3wh3JtObuR7UYKZemSU6Cl5gHM8,2988
|
|
846
|
-
codenerix_email/__pycache__/models.cpython-310.pyc,sha256=
|
|
847
|
-
codenerix_email/__pycache__/models.cpython-311.pyc,sha256=
|
|
846
|
+
codenerix_email/__pycache__/models.cpython-310.pyc,sha256=mGqvUrM4y9R0Rj_zCjrVQPLAs67eIjktc7LDslTJrMo,18271
|
|
847
|
+
codenerix_email/__pycache__/models.cpython-311.pyc,sha256=82FmyGqoonmz3JH91BClUvlTjIn43F3HB53lzq2-T38,37135
|
|
848
848
|
codenerix_email/__pycache__/models.cpython-35.pyc,sha256=oGheSKlh8Ttc6bB-qZ1JY5_6RySM9M6GY5-GCATxfes,9481
|
|
849
849
|
codenerix_email/__pycache__/models.cpython-39.pyc,sha256=ugyNNDG3k6rqsunwlcUIWADaLNvJ8snCgQwMZu9Ahac,8849
|
|
850
850
|
codenerix_email/__pycache__/test_settings.cpython-310.pyc,sha256=HH5CsPxLlq7_jW4ZCGBc1KzcV4HNQcI6YTh0wcz_F0w,715
|
|
@@ -867,12 +867,12 @@ codenerix_email/management/__pycache__/__init__.cpython-311.pyc,sha256=OtPcxwWjt
|
|
|
867
867
|
codenerix_email/management/__pycache__/__init__.cpython-35.pyc,sha256=sBoEWs6zdI0al-7t1deW9SE_Ln2RNDl2LyIiOO9gfRA,160
|
|
868
868
|
codenerix_email/management/__pycache__/__init__.cpython-39.pyc,sha256=uPXklfliVd3b8pLOJQT9ZeKcqmJMrGychvt68BsPulY,168
|
|
869
869
|
codenerix_email/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
870
|
-
codenerix_email/management/commands/email_test.py,sha256=
|
|
871
|
-
codenerix_email/management/commands/emails_recv.py,sha256=
|
|
872
|
-
codenerix_email/management/commands/emails_send.py,sha256=
|
|
873
|
-
codenerix_email/management/commands/recv_emails.py,sha256=
|
|
874
|
-
codenerix_email/management/commands/send_emails.py,sha256=
|
|
875
|
-
codenerix_email/management/commands/test_email.py,sha256=
|
|
870
|
+
codenerix_email/management/commands/email_test.py,sha256=SPsiq0s48sEX_G3piNHq-c1gDOH800H2zTvM19ei2LY,5605
|
|
871
|
+
codenerix_email/management/commands/emails_recv.py,sha256=RsUfv5s0XMReQ1Y0LjfLp-LJ4F7nFX2z_T1-QAAWcYY,24897
|
|
872
|
+
codenerix_email/management/commands/emails_send.py,sha256=scCFklro4WVMYm-1ataSjUMsPT-Ie5u_DdA55CQcTCQ,7944
|
|
873
|
+
codenerix_email/management/commands/recv_emails.py,sha256=aXmhdXlamiNxRpMIDSKBXUBhkOcwi5l_Pme7jSQUCME,273
|
|
874
|
+
codenerix_email/management/commands/send_emails.py,sha256=a1MnpvZKAEFdXNfmI5oFUkVxy4PZ1AjaJS6GH90zeD0,273
|
|
875
|
+
codenerix_email/management/commands/test_email.py,sha256=ZRAQ7vlPVAtrulaDC7yWeY_IZlgACIdgSiZdhiwCcn0,270
|
|
876
876
|
codenerix_email/management/commands/.mypy_cache/.gitignore,sha256=amnaZw0RUw038PDP3HvtMLeOpkNOJPenMgi5guKdMiw,34
|
|
877
877
|
codenerix_email/management/commands/.mypy_cache/CACHEDIR.TAG,sha256=8cE6_FVTWMkDOw8fMKqhd_6IvaQPS4okWYQA1UeHatw,190
|
|
878
878
|
codenerix_email/management/commands/.mypy_cache/3.10/@plugins_snapshot.json,sha256=RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o,2
|
|
@@ -1612,11 +1612,14 @@ codenerix_email/management/commands/.mypy_cache/3.10/zoneinfo/_tzpath.data.json,
|
|
|
1612
1612
|
codenerix_email/management/commands/.mypy_cache/3.10/zoneinfo/_tzpath.meta.json,sha256=OYk8PQ7lBy8EhzEZ-wVPBdLbz4UN_aH6SEappc-kQnI,1765
|
|
1613
1613
|
codenerix_email/management/commands/__pycache__/__init__.cpython-310.pyc,sha256=3-VfdLuaiBIg4KTIy7GETYTt2-AbfCU0vlH6fFZx7_M,189
|
|
1614
1614
|
codenerix_email/management/commands/__pycache__/__init__.cpython-311.pyc,sha256=yiqmtIhQMYXWx7g7XT-mvQNgGu_X2ymasWvVxIqPsBE,211
|
|
1615
|
+
codenerix_email/management/commands/__pycache__/email_test.cpython-311.pyc,sha256=ql-fe6qem8jGQCu-7Wm3ZCEvzOiVMsCdy0IxOyGnJbo,5946
|
|
1616
|
+
codenerix_email/management/commands/__pycache__/emails_recv.cpython-311.pyc,sha256=yhLzihKfS8KIBdf_WKNYaRuCpP5Lhv57Ui5eue0_hnk,20952
|
|
1617
|
+
codenerix_email/management/commands/__pycache__/emails_send.cpython-311.pyc,sha256=XSOhv92hH9G4Z9juLmXFTOLCS-oggZBA2TFyKIsxmd8,6868
|
|
1615
1618
|
codenerix_email/management/commands/__pycache__/recv_emails.cpython-310.pyc,sha256=qzj8puxw6pRAz_ptltybySs2mybOwdoZ8LB7Fw8YMGc,9897
|
|
1616
|
-
codenerix_email/management/commands/__pycache__/recv_emails.cpython-311.pyc,sha256=
|
|
1617
|
-
codenerix_email/management/commands/__pycache__/send_emails.cpython-311.pyc,sha256=
|
|
1619
|
+
codenerix_email/management/commands/__pycache__/recv_emails.cpython-311.pyc,sha256=YJoyWZ8Ax_ej7u9_YKjtru8BN8bKlMuvqNiTsLoWYUw,643
|
|
1620
|
+
codenerix_email/management/commands/__pycache__/send_emails.cpython-311.pyc,sha256=lqi8IebQlAUlOkceIp7QSYs1FyNYPG0znfxkPZm5guE,643
|
|
1618
1621
|
codenerix_email/management/commands/__pycache__/test_email.cpython-310.pyc,sha256=5X7qBWFWs7AsNgTo1DyQE0zi5gtLxiFIcwdnboZIUSg,3154
|
|
1619
|
-
codenerix_email/management/commands/__pycache__/test_email.cpython-311.pyc,sha256=
|
|
1622
|
+
codenerix_email/management/commands/__pycache__/test_email.cpython-311.pyc,sha256=pfqrykd9U6f9Droyne64rkSfGIqQauTADCEKsYXgsiQ,639
|
|
1620
1623
|
codenerix_email/migrations/0001_initial.py,sha256=bu1RU_FLeGKBjAXvOPHLUj2Ej9Ibns9PvE1CTVJf6qU,5816
|
|
1621
1624
|
codenerix_email/migrations/0002_auto_20170502_1043.py,sha256=-zoc4RuZFXJA1Fw8ECCVqAg-PYfku3yxdtYNyXPI3LM,2369
|
|
1622
1625
|
codenerix_email/migrations/0003_auto_20170921_1206.py,sha256=ncVdyZJ616vQpllGdaPbFS0n9qKfDP-TuVA5HkbPf4I,656
|
|
@@ -1684,8 +1687,8 @@ codenerix_email/migrations/__pycache__/__init__.cpython-35.pyc,sha256=2g70xiMW6o
|
|
|
1684
1687
|
codenerix_email/migrations/__pycache__/__init__.cpython-39.pyc,sha256=qNj2NH0YvoWPnCKxkVZPsEFsbM05y7t1njMskNISdVQ,168
|
|
1685
1688
|
codenerix_email/static/codenerix_email/emailmessages_rows.html,sha256=NyZpKPSHAAIywJX2ncS0H2bkqOVtMUwAdbYmkC4dKmk,2202
|
|
1686
1689
|
codenerix_email/static/codenerix_email/emailreceiveds_rows.html,sha256=k7-Qe7Y9mrKRJXl69aVe5hHUvGdyrLbMoh9gkpyCG2U,1976
|
|
1687
|
-
django_codenerix_email-4.0.
|
|
1688
|
-
django_codenerix_email-4.0.
|
|
1689
|
-
django_codenerix_email-4.0.
|
|
1690
|
-
django_codenerix_email-4.0.
|
|
1691
|
-
django_codenerix_email-4.0.
|
|
1690
|
+
django_codenerix_email-4.0.36.dist-info/LICENSE,sha256=IXMIpi75XsrJt1Sznt4EftT9c_4X0C9eqK4tHhH8H48,11339
|
|
1691
|
+
django_codenerix_email-4.0.36.dist-info/METADATA,sha256=BKIq8MArguzQgECY3LS2lyBk6jWzRfdy1OcEKhqBKVE,2676
|
|
1692
|
+
django_codenerix_email-4.0.36.dist-info/WHEEL,sha256=z9j0xAa_JmUKMpmz72K0ZGALSM_n-wQVmGbleXx2VHg,110
|
|
1693
|
+
django_codenerix_email-4.0.36.dist-info/top_level.txt,sha256=lljSA0iKE_UBEM5gIrGQwioC_i8Jjnp-aR1LFElENgw,16
|
|
1694
|
+
django_codenerix_email-4.0.36.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{django_codenerix_email-4.0.35.dist-info → django_codenerix_email-4.0.36.dist-info}/top_level.txt
RENAMED
|
File without changes
|