django-codenerix-email 4.0.37__py2.py3-none-any.whl → 4.0.39__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.
@@ -1,4 +1,4 @@
1
- __version__ = "4.0.37"
1
+ __version__ = "4.0.39"
2
2
 
3
3
  __authors__ = [
4
4
  "Juan Miguel Taboada Godoy <juanmi@juanmitaboada.com>",
@@ -1,4 +1,6 @@
1
1
  import re
2
+ from textwrap import dedent
3
+ from argparse import RawTextHelpFormatter
2
4
 
3
5
  import logging
4
6
 
@@ -31,7 +33,94 @@ from imapclient.exceptions import LoginError # noqa: E402
31
33
  class Command(BaseCommand):
32
34
  help = "Fetches new emails from the configured IMAP account."
33
35
 
36
+ def create_parser(self, prog_name, subcommand, **kwargs):
37
+ """
38
+ Create and return the ArgumentParser instance for this command.
39
+ We are overriding this to use the RawTextHelpFormatter.
40
+ """
41
+ parser = super().create_parser(prog_name, subcommand, **kwargs)
42
+ parser.formatter_class = RawTextHelpFormatter
43
+ return parser
44
+
34
45
  def add_arguments(self, parser):
46
+ parser.epilog = dedent(
47
+ r"""
48
+ This command connects to the configured IMAP server, fetches new
49
+ emails, and saves them as ReceivedEmail objects in the database.
50
+
51
+ By default, it processes only unseen emails. You can use the
52
+ --all option to process all emails in the inbox.
53
+
54
+ You can filter by specific IMAP ID, Message-ID, or Tracking ID
55
+ using the respective options.
56
+
57
+ The --rewrite option allows overwriting existing received emails
58
+ with the same Message-ID.
59
+
60
+ Examples:
61
+ # Fetch unseen emails
62
+ python manage.py emails_recv
63
+
64
+ # Fetch all emails
65
+ python manage.py emails_recv --all
66
+
67
+ # Fetch email by specific IMAP ID
68
+ python manage.py emails_recv --imap-id 123
69
+
70
+ # Fetch email by specific Message-ID
71
+ python manage.py emails_recv --message-id "<...>"
72
+
73
+ # Fetch email by specific Tracking ID
74
+ python manage.py emails_recv --tracking-id "uuid"
75
+
76
+ Make sure to configure the IMAP settings in your Django settings:
77
+ IMAP_EMAIL_HOST = "imap.example.com"
78
+ IMAP_EMAIL_PORT = 993
79
+ IMAP_EMAIL_USER = "your_username"
80
+ IMAP_EMAIL_PASSWORD = "your_password"
81
+ IMAP_EMAIL_SSL = True # (default: True)
82
+ IMAP_EMAIL_INBOX_FOLDER = "INBOX" # (default: "INBOX")
83
+ IMAP_EMAIL_SELECTOR = "UNSEEN" # (default: "UNSEEN")
84
+ IMAP_EMAIL_SEEN = True # (default: True)
85
+ IMAP_EMAIL_DELETE = False # (default: False)
86
+ IMAP_EMAIL_FILTERS = {
87
+ "SUBJECT": [r".*"],
88
+ "FROM": [r".*"],
89
+ "MESSAGE-ID": [r".*"],
90
+ "TO": [
91
+ r"^bounce@becas\.com",
92
+ r"^bounces@becas\.com",
93
+ r"^no-reply@becas\.com",
94
+ r"^hola@becas\.com",
95
+ r"^tuasesor@becas\.com",
96
+ r"^[a-zA-Z0-9._%+-]+@becas\.com", # *@becas.com
97
+ ],
98
+ "BODY_PLAIN": [r".*"],
99
+ "BODY_HTML": [r".*"],
100
+ "HEADER": [("X-Custom-Header", r".*")],
101
+ "BOUNCE_TYPE": ["hard", "soft"],
102
+ "BOUNCE_REASON": [r".*"],
103
+ "TRACKING_ID": True,
104
+ }
105
+
106
+ Filters are applied using AND logic across different fields and
107
+ OR logic within the same field.
108
+
109
+ The filters works as follows:
110
+ - SUBJECT, FROM, MESSAGE-ID, TO, BODY_PLAIN, BODY_HTML:
111
+ regex match (case-insensitive) against the respective field.
112
+ - HEADER: tuple of (header name, regex) to match specific headers.
113
+ - BOUNCE_TYPE: "hard" or "soft" to filter by bounce type.
114
+ - BOUNCE_REASON: regex to match the bounce reason.
115
+ - TRACKING_ID: if True, only process emails with a tracking ID.
116
+
117
+ If IMAP_EMAIL_FILTERS is not set, all emails are processed.
118
+
119
+ Note: This command marks processed emails as read (Seen) to avoid
120
+ reprocessing them in future runs.
121
+ """ # noqa: E501
122
+ )
123
+
35
124
  # Named (optional) arguments
36
125
  parser.add_argument(
37
126
  "--silent",
@@ -205,7 +294,9 @@ class Command(BaseCommand):
205
294
 
206
295
  else:
207
296
  # Search by UNSEEN
208
- messages_ids = server.search(["UNSEEN"])
297
+ messages_ids = server.search(
298
+ [getattr(settings, "IMAP_EMAIL_SELECTOR", "UNSEEN")]
299
+ )
209
300
  if self.verbose:
210
301
  self.stdout.write(
211
302
  self.style.SUCCESS(
@@ -244,6 +335,8 @@ class Command(BaseCommand):
244
335
  efrom = msg.get("From")
245
336
  eto = msg.get("To")
246
337
  eid = msg.get("Message-ID")
338
+ ecc = msg.get("Cc")
339
+ ebc = msg.get("Bcc")
247
340
 
248
341
  # If we can't get a Message-ID, use the IMAP ID as fallback
249
342
  # to avoid duplicates
@@ -305,7 +398,8 @@ class Command(BaseCommand):
305
398
  self.stdout.write(
306
399
  self.style.WARNING(
307
400
  f"Tracking ID {tracking_id} found "
308
- "but no matching sent email."
401
+ f"for IMAP ID {imap_id} but no "
402
+ "matching sent email."
309
403
  )
310
404
  )
311
405
 
@@ -328,6 +422,47 @@ class Command(BaseCommand):
328
422
  )
329
423
  headers[header] = decoded_value
330
424
 
425
+ # Let emails pass based on filtering system
426
+ (filter_passed, filter_reason) = self.filter_pass(
427
+ subject,
428
+ efrom,
429
+ eto,
430
+ ecc,
431
+ ebc,
432
+ eid,
433
+ body_plain,
434
+ body_html,
435
+ headers,
436
+ bounce_type,
437
+ bounce_reason,
438
+ tracking_id,
439
+ )
440
+ if not filter_passed:
441
+ if self.verbose:
442
+ self.stdout.write(
443
+ self.style.NOTICE(
444
+ f"Skipping email with IMAP ID: {imap_id} "
445
+ f"(FILTER: {filter_reason})"
446
+ )
447
+ )
448
+
449
+ if getattr(settings, "IMAP_EMAIL_DELETE", False):
450
+ # Delete the message from the server
451
+ server.delete_messages(imap_id)
452
+ server.expunge()
453
+ if self.verbose:
454
+ self.stdout.write(
455
+ self.style.SUCCESS(
456
+ f"Deleted email with IMAP ID: {imap_id}"
457
+ )
458
+ )
459
+
460
+ elif getattr(settings, "IMAP_EMAIL_SEEN", True):
461
+ # Mark the message as read to avoid reprocessing
462
+ server.add_flags(imap_id, [b"\\Seen"])
463
+
464
+ continue
465
+
331
466
  # Create EmailReceived object if doesn't exist
332
467
  if not email_received:
333
468
  overwriting = False
@@ -360,7 +495,12 @@ class Command(BaseCommand):
360
495
  created_count += 1
361
496
  verb = "Created"
362
497
 
498
+ # Show info about the processed email
363
499
  if self.verbose:
500
+ if overwriting:
501
+ style = self.style.MIGRATE_HEADING
502
+ else:
503
+ style = self.style.WARNING
364
504
  msg = (
365
505
  f"{verb} email with IMAP ID: "
366
506
  f"{imap_id} (link={tracking_id})"
@@ -371,26 +511,40 @@ class Command(BaseCommand):
371
511
  )
372
512
  bounce_reason_str = bounce_reason or "Unknown"
373
513
  self.stdout.write(
374
- self.style.WARNING(
514
+ style(
375
515
  f"{msg} "
376
516
  f"[{bounce_type_str} bounce, "
377
517
  f"reason={bounce_reason_str}]"
378
518
  )
379
519
  )
380
520
  else:
381
- self.stdout.write(self.style.SUCCESS(msg))
521
+ if overwriting:
522
+ style = self.style.MIGRATE_HEADING
523
+ else:
524
+ style = self.style.SUCCESS
525
+ self.stdout.write(style(msg))
382
526
 
383
527
  else:
384
528
  if self.verbose:
385
529
  self.stdout.write(
386
- self.style.WARNING(
530
+ self.style.HTTP_INFO(
387
531
  f"Skipping email with IMAP ID: {imap_id} (DUP)"
388
532
  )
389
533
  )
390
534
 
391
- # Mark the message as read
392
- # (flag \Seen) avoid reprocessing
393
- server.add_flags(imap_id, [b"\\Seen"])
535
+ if getattr(settings, "IMAP_EMAIL_DELETE", False):
536
+ # Delete the message from the server
537
+ server.delete_messages(imap_id)
538
+ server.expunge()
539
+ if self.verbose:
540
+ self.stdout.write(
541
+ self.style.SUCCESS(
542
+ f"Deleted email with IMAP ID: {imap_id}"
543
+ )
544
+ )
545
+ elif getattr(settings, "IMAP_EMAIL_SEEN", True):
546
+ # Mark the message as read otherwise (to avoid reprocessing)
547
+ server.add_flags(imap_id, [b"\\Seen"])
394
548
 
395
549
  return (created_count, overwrite_count)
396
550
 
@@ -599,3 +753,112 @@ class Command(BaseCommand):
599
753
  bounce_reason = "Unknown (Subject Keyword)"
600
754
 
601
755
  return (bounce_type, bounce_reason)
756
+
757
+ def filter_pass(
758
+ self,
759
+ subject: str,
760
+ efrom: str,
761
+ eto: str,
762
+ ecc: str,
763
+ ebc: str,
764
+ eid: str,
765
+ body_plain: str,
766
+ body_html: str,
767
+ headers: dict,
768
+ bounce_type: Optional[str],
769
+ bounce_reason: Optional[str],
770
+ tracking_id: Optional[str],
771
+ ) -> tuple[bool, str]:
772
+ """
773
+ Applies filtering rules to determine if an email should be processed.
774
+
775
+ Returns:
776
+ True if the email should be processed, False otherwise.
777
+ """
778
+
779
+ # Get filters from settings
780
+ filters = getattr(settings, "IMAP_EMAIL_FILTERS", None)
781
+ if not filters:
782
+ # No filters defined, allow processing
783
+ return (True, "No filters defined, processing all.")
784
+
785
+ # Helper function to apply regex list to a value
786
+ def match_any(value: str, patterns: list) -> bool:
787
+ for pattern in patterns:
788
+ if re.search(pattern, value, re.IGNORECASE):
789
+ return True # Match found
790
+ return False # No matches
791
+
792
+ # Apply filters
793
+ for key, patterns in filters.items():
794
+ if key == "SUBJECT" and patterns:
795
+ if not match_any(subject or "", patterns):
796
+ return (False, f"SUBJECT failed: {subject}")
797
+
798
+ elif key == "FROM" and patterns:
799
+ if not match_any(efrom or "", patterns):
800
+ return (False, f"FROM failed: {efrom}")
801
+
802
+ elif key == "TO" and patterns:
803
+ if not any(
804
+ match_any(targets or "", patterns)
805
+ for targets in (
806
+ eto or "",
807
+ ecc or "",
808
+ ebc or "",
809
+ )
810
+ ):
811
+ # If no direct match, try more robust email extraction
812
+ # Analize only the email addresses
813
+ emails = re.split(r"[;,]\s*", eto or "")
814
+ emails += re.split(r"[;,]\s*", ecc or "")
815
+ emails += re.split(r"[;,]\s*", ebc or "")
816
+
817
+ # Stay only with the email and strip < and > if present
818
+ email_only = [
819
+ e.split()[-1].strip("<>") for e in emails if e
820
+ ]
821
+
822
+ # Check if any of the emails match
823
+ if not any(match_any(e, patterns) for e in email_only):
824
+ return (False, f"TO failed: {eto}")
825
+
826
+ elif key == "MESSAGE-ID" and patterns:
827
+ if not match_any(eid or "", patterns):
828
+ return (False, f"MESSAGE-ID failed: {eid}")
829
+
830
+ elif key == "BODY_PLAIN" and patterns:
831
+ if not match_any(body_plain or "", patterns):
832
+ return (False, f"BODY_PLAIN failed: {body_plain}")
833
+
834
+ elif key == "BODY_HTML" and patterns:
835
+ if not match_any(body_html or "", patterns):
836
+ return (False, f"BODY_HTML failed: {body_html}")
837
+
838
+ elif key == "HEADER" and patterns:
839
+ header_matched = False
840
+ for header_name, header_patterns in patterns:
841
+ header_value = headers.get(header_name, "")
842
+ if match_any(header_value, header_patterns):
843
+ header_matched = True
844
+ break
845
+ if not header_matched:
846
+ return (False, f"HEADER failed: {headers}")
847
+
848
+ elif key == "BOUNCE_TYPE" and patterns:
849
+ if bounce_type not in patterns:
850
+ return (False, f"BOUNCE_TYPE failed: {bounce_type}")
851
+
852
+ elif key == "BOUNCE_REASON" and patterns:
853
+ if not bounce_reason or not match_any(bounce_reason, patterns):
854
+ return (
855
+ False,
856
+ f"BOUNCE_REASON failed: {bounce_reason}",
857
+ )
858
+
859
+ elif key == "TRACKING_ID" and patterns:
860
+ if patterns and not tracking_id:
861
+ return (False, "TRACKING_ID failed: No tracking ID")
862
+
863
+ # If all filters passed, allow processing
864
+ return (True, "All filters passed.")
@@ -12,7 +12,9 @@
12
12
  class="text-nowrap">{{row.eto|codenerix}}</td>
13
13
  <td
14
14
  ng-class="{'info': (!row.email__pk) && (!row.bounce_type), 'warning': row.bounce_type=='S', 'danger': row.bounce_type=='H', 'text-warning': row.bounce_type=='S', 'text-danger': row.bounce_type=='H'}"
15
- class="text-nowrap">{{row.subject|codenerix}}</td>
15
+ class="text-nowrap">
16
+ <span codenerix-html-compile='row.subject|codenerix:"shorttext:50"'></span>
17
+ </td>
16
18
  <td
17
19
  ng-class="{'info': (!row.email__pk) && (!row.bounce_type), 'warning': row.bounce_type=='S', 'danger': row.bounce_type=='H', 'text-warning': row.bounce_type=='S', 'text-danger': row.bounce_type=='H'}"
18
20
  ng-click="$event.stopPropagation();"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-codenerix-email
3
- Version: 4.0.37
3
+ Version: 4.0.39
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=nmegdQqWfQuMKIEdzqRtGCeg5es1m1w4_DZMJfzSRcg,149
1
+ codenerix_email/__init__.py,sha256=jy3doviOd16K4l2OxN9UBbjhtzx2zjQmwaHgbqXosnw,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=BVGH5_FfAI_IwJgqrXXzP-rsKn-MC6qCbtu3xJnll0g,313
831
- codenerix_email/__pycache__/__init__.cpython-311.pyc,sha256=ifkZHL6PJ-TFRdhWEpJ9r1KPdQOFN5_n_-lFhu8UVzE,337
830
+ codenerix_email/__pycache__/__init__.cpython-310.pyc,sha256=jR8Y-ZqrC_AzgwY3EjpVl3TOkhtTgWepEoei20YfoQw,313
831
+ codenerix_email/__pycache__/__init__.cpython-311.pyc,sha256=VHHEae6bOdMZRv4BoHz1g5HaZMN7_WmD3aXcpvu2mAY,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
@@ -868,7 +868,7 @@ codenerix_email/management/__pycache__/__init__.cpython-35.pyc,sha256=sBoEWs6zdI
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
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
871
+ codenerix_email/management/commands/emails_recv.py,sha256=4QfIQ2M97T4Y3o7BR1d4nA8UF8bqe81wCMUpiooSzxs,35872
872
872
  codenerix_email/management/commands/emails_send.py,sha256=scCFklro4WVMYm-1ataSjUMsPT-Ie5u_DdA55CQcTCQ,7944
873
873
  codenerix_email/management/commands/recv_emails.py,sha256=aXmhdXlamiNxRpMIDSKBXUBhkOcwi5l_Pme7jSQUCME,273
874
874
  codenerix_email/management/commands/send_emails.py,sha256=a1MnpvZKAEFdXNfmI5oFUkVxy4PZ1AjaJS6GH90zeD0,273
@@ -1613,7 +1613,7 @@ codenerix_email/management/commands/.mypy_cache/3.10/zoneinfo/_tzpath.meta.json,
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
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
1616
+ codenerix_email/management/commands/__pycache__/emails_recv.cpython-311.pyc,sha256=2xdfhIh76dxEnxx7wxaDsCeycJM4wRCb14C0CJax6to,30330
1617
1617
  codenerix_email/management/commands/__pycache__/emails_send.cpython-311.pyc,sha256=XSOhv92hH9G4Z9juLmXFTOLCS-oggZBA2TFyKIsxmd8,6868
1618
1618
  codenerix_email/management/commands/__pycache__/recv_emails.cpython-310.pyc,sha256=qzj8puxw6pRAz_ptltybySs2mybOwdoZ8LB7Fw8YMGc,9897
1619
1619
  codenerix_email/management/commands/__pycache__/recv_emails.cpython-311.pyc,sha256=YJoyWZ8Ax_ej7u9_YKjtru8BN8bKlMuvqNiTsLoWYUw,643
@@ -1686,9 +1686,9 @@ codenerix_email/migrations/__pycache__/__init__.cpython-311.pyc,sha256=RbbUUEhcJ
1686
1686
  codenerix_email/migrations/__pycache__/__init__.cpython-35.pyc,sha256=2g70xiMW6oJNkIpRM-0Dr5h7AUac-3xyCXPONxp9BBw,147
1687
1687
  codenerix_email/migrations/__pycache__/__init__.cpython-39.pyc,sha256=qNj2NH0YvoWPnCKxkVZPsEFsbM05y7t1njMskNISdVQ,168
1688
1688
  codenerix_email/static/codenerix_email/emailmessages_rows.html,sha256=NyZpKPSHAAIywJX2ncS0H2bkqOVtMUwAdbYmkC4dKmk,2202
1689
- codenerix_email/static/codenerix_email/emailreceiveds_rows.html,sha256=k7-Qe7Y9mrKRJXl69aVe5hHUvGdyrLbMoh9gkpyCG2U,1976
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,,
1689
+ codenerix_email/static/codenerix_email/emailreceiveds_rows.html,sha256=u9DXVdzKhx9WlJgcK_cT2DfGp9bGIKTBn3LDQplB4g4,2032
1690
+ django_codenerix_email-4.0.39.dist-info/LICENSE,sha256=IXMIpi75XsrJt1Sznt4EftT9c_4X0C9eqK4tHhH8H48,11339
1691
+ django_codenerix_email-4.0.39.dist-info/METADATA,sha256=et_AOcJOU0_8V_DL_Br12lR3UkKeJEI06F1mH-A9o2k,2676
1692
+ django_codenerix_email-4.0.39.dist-info/WHEEL,sha256=z9j0xAa_JmUKMpmz72K0ZGALSM_n-wQVmGbleXx2VHg,110
1693
+ django_codenerix_email-4.0.39.dist-info/top_level.txt,sha256=lljSA0iKE_UBEM5gIrGQwioC_i8Jjnp-aR1LFElENgw,16
1694
+ django_codenerix_email-4.0.39.dist-info/RECORD,,