django-codenerix-email 4.0.35__py2.py3-none-any.whl → 4.0.37__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.
Files changed (23) hide show
  1. codenerix_email/__init__.py +1 -1
  2. codenerix_email/__pycache__/__init__.cpython-310.pyc +0 -0
  3. codenerix_email/__pycache__/__init__.cpython-311.pyc +0 -0
  4. codenerix_email/__pycache__/models.cpython-310.pyc +0 -0
  5. codenerix_email/__pycache__/models.cpython-311.pyc +0 -0
  6. codenerix_email/management/commands/__pycache__/email_test.cpython-311.pyc +0 -0
  7. codenerix_email/management/commands/__pycache__/emails_recv.cpython-311.pyc +0 -0
  8. codenerix_email/management/commands/__pycache__/emails_send.cpython-311.pyc +0 -0
  9. codenerix_email/management/commands/__pycache__/recv_emails.cpython-311.pyc +0 -0
  10. codenerix_email/management/commands/__pycache__/send_emails.cpython-311.pyc +0 -0
  11. codenerix_email/management/commands/__pycache__/test_email.cpython-311.pyc +0 -0
  12. codenerix_email/management/commands/email_test.py +0 -10
  13. codenerix_email/management/commands/emails_recv.py +0 -10
  14. codenerix_email/management/commands/emails_send.py +0 -10
  15. codenerix_email/management/commands/recv_emails.py +1 -601
  16. codenerix_email/management/commands/send_emails.py +2 -239
  17. codenerix_email/management/commands/test_email.py +1 -174
  18. codenerix_email/models.py +0 -13
  19. {django_codenerix_email-4.0.35.dist-info → django_codenerix_email-4.0.37.dist-info}/METADATA +1 -1
  20. {django_codenerix_email-4.0.35.dist-info → django_codenerix_email-4.0.37.dist-info}/RECORD +23 -20
  21. {django_codenerix_email-4.0.35.dist-info → django_codenerix_email-4.0.37.dist-info}/LICENSE +0 -0
  22. {django_codenerix_email-4.0.35.dist-info → django_codenerix_email-4.0.37.dist-info}/WHEEL +0 -0
  23. {django_codenerix_email-4.0.35.dist-info → django_codenerix_email-4.0.37.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- __version__ = "4.0.35"
1
+ __version__ = "4.0.37"
2
2
 
3
3
  __authors__ = [
4
4
  "Juan Miguel Taboada Godoy <juanmi@juanmitaboada.com>",
@@ -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: 'send_email' is DEPRECATED, switch to 'emails_send' instead"
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
codenerix_email/models.py CHANGED
@@ -371,20 +371,7 @@ class EmailMessage(CodenerixModel):
371
371
  legacy=False,
372
372
  silent=True,
373
373
  debug=False,
374
- content_subtype=None,
375
374
  ):
376
- # Autoconfigure Debugger
377
-
378
- # Warn about subtype
379
- if content_subtype:
380
- logger.warning(
381
- _(
382
- "Programming ERROR: You are using content_subtype, this "
383
- "value has been DEPRECATED and will be remove in future "
384
- "versions."
385
- )
386
- )
387
-
388
375
  # Get connection if not connected yet
389
376
  if connection is None:
390
377
  # Connect
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-codenerix-email
3
- Version: 4.0.35
3
+ Version: 4.0.37
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,8 +1,8 @@
1
- codenerix_email/__init__.py,sha256=aqCRJ0xgtc7E3Ql9fYCsLEo4p-pMITa-D9LKQCuhnNo,149
1
+ codenerix_email/__init__.py,sha256=nmegdQqWfQuMKIEdzqRtGCeg5es1m1w4_DZMJfzSRcg,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
5
- codenerix_email/models.py,sha256=SwjybHsg919OPgltvv6M9AHGxoql5sboluHRyv4WRjk,25700
5
+ codenerix_email/models.py,sha256=e1BqaycD0sVTYwpeUlow50ijyW55Da-9kQDXaABxZrw,25311
6
6
  codenerix_email/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  codenerix_email/test_settings.py,sha256=A9WT-2MMuqvW5Mrd3lkmjrf0xJZmJl3McOEeyJug1Xc,633
8
8
  codenerix_email/urls.py,sha256=M760qDSVV3EoY9aIPdnk8DtUv5tyWO-FQWdody4KTeM,3996
@@ -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=Sq5FOgkgx3zOEtAjCNOIlXtD0lb96C_KrdSvT2UtZA0,313
831
- codenerix_email/__pycache__/__init__.cpython-311.pyc,sha256=zDkK7LZsQ9OHEfqGbRyF68_QZn1r1Q0V1zE4XMsDiPk,337
830
+ codenerix_email/__pycache__/__init__.cpython-310.pyc,sha256=BVGH5_FfAI_IwJgqrXXzP-rsKn-MC6qCbtu3xJnll0g,313
831
+ codenerix_email/__pycache__/__init__.cpython-311.pyc,sha256=ifkZHL6PJ-TFRdhWEpJ9r1KPdQOFN5_n_-lFhu8UVzE,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=L3f8aE3uhDm7R0tBfGFojEi-ueYkHoPIxaAaoDd-0ls,18271
847
- codenerix_email/__pycache__/models.cpython-311.pyc,sha256=5UAVckTk83Psun4_RV_MCfG4UCcSoj435IEZ0ialb3A,37032
846
+ codenerix_email/__pycache__/models.cpython-310.pyc,sha256=tJ-ff7rjubUvp21PUTwvWd5LAKZzRCOXzXJO8-aGtTc,18097
847
+ codenerix_email/__pycache__/models.cpython-311.pyc,sha256=_ketRfcsx3QB9uplTrFGkqs_D69iM_lwL_UYziboJBc,36756
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=byeqE_-woEkml_PoQxwr9zp0XfCruOYAoVfJBIkQr24,5847
871
- codenerix_email/management/commands/emails_recv.py,sha256=kzSdVPFsuy8_FH8PkEP41IU3-dD114q1NkUGVeLAzfs,25141
872
- codenerix_email/management/commands/emails_send.py,sha256=qH87OgRH1rPMYbcm0Q6Pgyf38YGXgYhhcFP0Jvkeps8,8187
873
- codenerix_email/management/commands/recv_emails.py,sha256=kzSdVPFsuy8_FH8PkEP41IU3-dD114q1NkUGVeLAzfs,25141
874
- codenerix_email/management/commands/send_emails.py,sha256=qH87OgRH1rPMYbcm0Q6Pgyf38YGXgYhhcFP0Jvkeps8,8187
875
- codenerix_email/management/commands/test_email.py,sha256=byeqE_-woEkml_PoQxwr9zp0XfCruOYAoVfJBIkQr24,5847
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=LBMH_iYJ9A60KPpUpDzfW_RZgRRdm-aklGpdDPp36TI,20952
1617
- codenerix_email/management/commands/__pycache__/send_emails.cpython-311.pyc,sha256=DjQ_opoCdF-yWwjsXgsNU0QRwB5lFpD57AiJkBvK6K4,7259
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=fhnAA80N3Tv4sdbJQXG7MswXmlfvl9aEZFd7Uyqsoi4,6334
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.35.dist-info/LICENSE,sha256=IXMIpi75XsrJt1Sznt4EftT9c_4X0C9eqK4tHhH8H48,11339
1688
- django_codenerix_email-4.0.35.dist-info/METADATA,sha256=45BPU_K_ExxR_MIzqgqDZLQhrIje8tkLuxMid-HCjZA,2676
1689
- django_codenerix_email-4.0.35.dist-info/WHEEL,sha256=z9j0xAa_JmUKMpmz72K0ZGALSM_n-wQVmGbleXx2VHg,110
1690
- django_codenerix_email-4.0.35.dist-info/top_level.txt,sha256=lljSA0iKE_UBEM5gIrGQwioC_i8Jjnp-aR1LFElENgw,16
1691
- django_codenerix_email-4.0.35.dist-info/RECORD,,
1690
+ django_codenerix_email-4.0.37.dist-info/LICENSE,sha256=IXMIpi75XsrJt1Sznt4EftT9c_4X0C9eqK4tHhH8H48,11339
1691
+ django_codenerix_email-4.0.37.dist-info/METADATA,sha256=Y84bu_edEiA6UoycJZCYyUVES1Z9JrJWzOrHVUhrsPI,2676
1692
+ django_codenerix_email-4.0.37.dist-info/WHEEL,sha256=z9j0xAa_JmUKMpmz72K0ZGALSM_n-wQVmGbleXx2VHg,110
1693
+ django_codenerix_email-4.0.37.dist-info/top_level.txt,sha256=lljSA0iKE_UBEM5gIrGQwioC_i8Jjnp-aR1LFElENgw,16
1694
+ django_codenerix_email-4.0.37.dist-info/RECORD,,