epstein-files 1.1.3__tar.gz → 1.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {epstein_files-1.1.3 → epstein_files-1.1.5}/PKG-INFO +1 -1
  2. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/__init__.py +10 -6
  3. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/documents/communication.py +3 -3
  4. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/documents/document.py +3 -0
  5. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/documents/email.py +77 -57
  6. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/documents/imessage/text_message.py +5 -9
  7. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/documents/messenger_log.py +2 -2
  8. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/epstein_files.py +15 -13
  9. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/constant/names.py +39 -47
  10. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/constant/strings.py +1 -0
  11. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/constants.py +64 -8
  12. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/data.py +9 -1
  13. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/doc_cfg.py +8 -2
  14. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/file_helper.py +4 -1
  15. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/highlighted_group.py +81 -46
  16. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/output.py +46 -35
  17. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/rich.py +26 -33
  18. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/word_count.py +1 -2
  19. {epstein_files-1.1.3 → epstein_files-1.1.5}/pyproject.toml +1 -1
  20. {epstein_files-1.1.3 → epstein_files-1.1.5}/LICENSE +0 -0
  21. {epstein_files-1.1.3 → epstein_files-1.1.5}/README.md +0 -0
  22. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/documents/emails/email_header.py +0 -0
  23. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/documents/json_file.py +0 -0
  24. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/documents/other_file.py +0 -0
  25. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/constant/common_words.py +0 -0
  26. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/constant/html.py +0 -0
  27. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/constant/output_files.py +0 -0
  28. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/constant/urls.py +0 -0
  29. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/env.py +0 -0
  30. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/logging.py +0 -0
  31. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/search_result.py +0 -0
  32. {epstein_files-1.1.3 → epstein_files-1.1.5}/epstein_files/util/timer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: epstein-files
3
- Version: 1.1.3
3
+ Version: 1.1.5
4
4
  Summary: Tools for working with the Jeffrey Epstein documents released in November 2025.
5
5
  Home-page: https://michelcrypt4d4mus.github.io/epstein_text_messages/
6
6
  License: GPL-3.0-or-later
@@ -44,7 +44,7 @@ def generate_html() -> None:
44
44
  print_json_files(epstein_files)
45
45
  exit()
46
46
 
47
- print_title_page_header(epstein_files)
47
+ print_title_page_header()
48
48
 
49
49
  if args.email_timeline:
50
50
  print_color_key()
@@ -96,8 +96,7 @@ def epstein_search():
96
96
  for search_term in args.positional_args:
97
97
  temp_highlighter = build_highlighter(search_term)
98
98
  search_results = epstein_files.docs_matching(search_term, args.names)
99
- console.line(2)
100
- print_subtitle_panel(f"Found {len(search_results)} documents matching '{search_term}'", padding=(0, 0, 0, 3))
99
+ print_subtitle_panel(f"Found {len(search_results)} documents matching '{search_term}'")
101
100
 
102
101
  for search_result in search_results:
103
102
  console.line()
@@ -115,11 +114,16 @@ def epstein_search():
115
114
  def epstein_show():
116
115
  """Show the color highlighted file. If --raw arg is passed, show the raw text of the file as well."""
117
116
  _assert_positional_args()
118
- ids = [extract_file_id(arg) for arg in args.positional_args]
119
- raw_docs = [Document(coerce_file_path(id)) for id in ids]
120
- docs = [document_cls(doc)(doc.file_path) for doc in raw_docs]
117
+ raw_docs: list[Document] = []
121
118
  console.line()
122
119
 
120
+ try:
121
+ ids = [extract_file_id(arg) for arg in args.positional_args]
122
+ raw_docs = [Document(coerce_file_path(id)) for id in ids]
123
+ docs = Document.sort_by_timestamp([document_cls(doc)(doc.file_path) for doc in raw_docs])
124
+ except Exception as e:
125
+ exit_with_error(str(e))
126
+
123
127
  for doc in docs:
124
128
  console.print('\n', doc, '\n')
125
129
 
@@ -9,7 +9,7 @@ from epstein_files.documents.document import CLOSE_PROPERTIES_CHAR, Document
9
9
  from epstein_files.util.constant.names import UNKNOWN
10
10
  from epstein_files.util.constants import FALLBACK_TIMESTAMP
11
11
  from epstein_files.util.doc_cfg import CommunicationCfg
12
- from epstein_files.util.highlighted_group import get_style_for_name
12
+ from epstein_files.util.highlighted_group import get_style_for_name, styled_name
13
13
  from epstein_files.util.rich import key_value_txt
14
14
 
15
15
  TIMESTAMP_SECONDS_REGEX = re.compile(r":\d{2}$")
@@ -25,10 +25,10 @@ class Communication(Document):
25
25
  return self.author or UNKNOWN
26
26
 
27
27
  def author_style(self) -> str:
28
- return get_style_for_name(self.author_or_unknown())
28
+ return get_style_for_name(self.author)
29
29
 
30
30
  def author_txt(self) -> Text:
31
- return Text(self.author_or_unknown(), style=self.author_style())
31
+ return styled_name(self.author)
32
32
 
33
33
  def external_links_txt(self, _style: str = '', include_alt_links: bool = True) -> Text:
34
34
  """Overrides super() method to apply self.author_style."""
@@ -88,6 +88,9 @@ class Document:
88
88
  strip_whitespace: ClassVar[bool] = True # Overridden in JsonFile
89
89
 
90
90
  def __post_init__(self):
91
+ if not self.file_path.exists():
92
+ raise FileNotFoundError(f"File '{self.file_path.name}' does not exist!")
93
+
91
94
  self.filename = self.file_path.name
92
95
  self.file_id = extract_file_id(self.filename)
93
96
  # config and url_slug could have been pre-set in Email
@@ -24,7 +24,7 @@ from epstein_files.util.data import (TIMEZONE_INFO, collapse_newlines, escape_si
24
24
  flatten, listify, remove_timezone, uniquify)
25
25
  from epstein_files.util.doc_cfg import EmailCfg, Metadata
26
26
  from epstein_files.util.file_helper import extract_file_id, file_stem_for_id
27
- from epstein_files.util.highlighted_group import get_style_for_name
27
+ from epstein_files.util.highlighted_group import JUNK_EMAILERS, get_style_for_name
28
28
  from epstein_files.util.logging import logger
29
29
  from epstein_files.util.rich import *
30
30
 
@@ -71,6 +71,7 @@ OCR_REPAIRS: dict[str | re.Pattern, str] = {
71
71
  # Signatures
72
72
  'BlackBerry by AT &T': 'BlackBerry by AT&T',
73
73
  'BlackBerry from T- Mobile': 'BlackBerry from T-Mobile',
74
+ 'Envoy& de mon iPhone': 'Envoyé de mon iPhone',
74
75
  "from my 'Phone": 'from my iPhone',
75
76
  'from Samsung Mob.le': 'from Samsung Mobile',
76
77
  'gJeremyRubin': '@JeremyRubin',
@@ -126,6 +127,14 @@ EMAIL_SIGNATURE_REGEXES = {
126
127
  UNKNOWN: re.compile(r"(This message is directed to and is for the use of the above-noted addressee only.*\nhereon\.)", re.DOTALL),
127
128
  }
128
129
 
130
+ EMAIL_TABLE_COLS = [
131
+ {'name': 'Sent At', 'justify': 'left', 'style': TIMESTAMP_DIM},
132
+ {'name': 'From', 'justify': 'left', 'max_width': 20},
133
+ {'name': 'To', 'justify': 'left', 'max_width': 22},
134
+ {'name': 'Length', 'justify': 'right', 'style': 'wheat4'},
135
+ {'name': 'Subject', 'justify': 'left', 'min_width': 35, 'style': 'honeydew2'},
136
+ ]
137
+
129
138
  MAILING_LISTS = [
130
139
  CAROLYN_RANGEL,
131
140
  INTELLIGENCE_SQUARED,
@@ -141,11 +150,13 @@ TRUNCATE_ALL_EMAILS_FROM = JUNK_EMAILERS + MAILING_LISTS + [
141
150
 
142
151
  TRUNCATION_LENGTHS = {
143
152
  '023627': 16_800, # Micheal Wolff article with brock pierce
144
- '030245': 7_500, # Epstein rationalizes his behavior in an open letter to the world
145
- '030781': 1_700, # Bannon email about crypto coin issues
146
- '032906': 750, # David Blaine email
153
+ '030245': None, # Epstein rationalizes his behavior in an open letter to the world
154
+ '030781': None, # Bannon email about crypto coin issues
155
+ '032906': None, # David Blaine email
147
156
  '026036': 6000, # Gino Yu blockchain mention
148
- '023208': 350_000, # Long discussion about leon black's finances
157
+ '023208': None, # Long discussion about leon black's finances
158
+ '029609': None, # Joi Ito
159
+ '025233': None, # Reputation.com discussion
149
160
  }
150
161
 
151
162
  # These are long forwarded articles so we force a trim to 1,333 chars if these strings exist
@@ -287,14 +298,6 @@ USELESS_EMAILERS = FLIGHT_IN_2012_PEOPLE + IRAN_DEAL_RECIPIENTS + KRASSNER_RECIP
287
298
  'p.peachev@independent.co.uk',
288
299
  ]
289
300
 
290
- # Emails sent by epstein to himself that are just notes
291
- SELF_EMAILS_FILE_IDS = [
292
- '026677',
293
- '029752', # TODO: jokeland...
294
- '030238',
295
- # '033274', # TODO: Epstein's note to self doesn't get printed if we don't set the recipients to [None]
296
- ]
297
-
298
301
  METADATA_FIELDS = [
299
302
  'is_junk_mail',
300
303
  'recipients',
@@ -311,6 +314,7 @@ LINE_REPAIR_MERGES = {
311
314
  '022695': 4,
312
315
  '023067': 3,
313
316
  '025790': 2,
317
+ '026345': 3,
314
318
  '026609': 4,
315
319
  '026924': [2, 4],
316
320
  '028931': [3, 6],
@@ -337,6 +341,7 @@ LINE_REPAIR_MERGES = {
337
341
  '032405': 4,
338
342
  '033097': 2,
339
343
  '033144': [2, 4],
344
+ '033217': 3,
340
345
  '033228': [3, 5],
341
346
  '033357': [2, 4],
342
347
  '033486': [7, 9],
@@ -382,25 +387,21 @@ class Email(Communication):
382
387
 
383
388
  super().__post_init__()
384
389
 
385
- try:
386
- if self.config and self.config.recipients:
387
- self.recipients = self.config.recipients
388
- else:
389
- for recipient in self.header.recipients():
390
- self.recipients.extend(self._extract_emailer_names(recipient))
391
-
392
- if self.author in MAILING_LISTS and (len(self.recipients) == 0 or self.recipients == [self.author]):
393
- self.recipients = [JEFFREY_EPSTEIN] # Assume mailing list emails are to Epstein
394
- except Exception as e:
395
- console.print_exception()
396
- console.line(2)
397
- logger.fatal(f"Failed on {self.file_id}")
398
- console.line(2)
399
- raise e
400
-
401
- # Remove self CCs
402
- recipients = [r for r in self.recipients if r != self.author or self.file_id in SELF_EMAILS_FILE_IDS]
403
- self.recipients = list(set(recipients))
390
+ if self.config and self.config.recipients:
391
+ self.recipients = self.config.recipients
392
+ else:
393
+ for recipient in self.header.recipients():
394
+ self.recipients.extend(self._extract_emailer_names(recipient))
395
+
396
+ # Assume mailing list emails are to Epstein
397
+ if self.author in MAILING_LISTS and (self.is_note_to_self() or not self.recipients):
398
+ self.recipients = [JEFFREY_EPSTEIN]
399
+
400
+ # Remove self CCs but preserve self emails
401
+ if not self.is_note_to_self():
402
+ self.recipients = [r for r in self.recipients if r != self.author]
403
+
404
+ self.recipients = sorted(list(set(self.recipients)), key=lambda r: r or UNKNOWN)
404
405
  self.text = self._prettify_text()
405
406
  self.actual_text = self._actual_text()
406
407
  self.sent_from_device = self._sent_from_device()
@@ -410,8 +411,13 @@ class Email(Communication):
410
411
 
411
412
  def info_txt(self) -> Text:
412
413
  email_type = 'fwded article' if self.is_fwded_article() else 'email'
413
- txt = Text(f"OCR text of {email_type} from ", style='grey46').append(self.author_txt()).append(' to ')
414
- return txt.append(self.recipients_txt()).append(highlighter(f" probably sent at {self.timestamp}"))
414
+ txt = Text(f"OCR text of {email_type} from ", style='grey46').append(self.author_txt())
415
+
416
+ if self.config and self.config.is_attribution_uncertain:
417
+ txt.append(f" {QUESTION_MARKS}", style=self.author_style())
418
+
419
+ txt.append(' to ').append(self.recipients_txt())
420
+ return txt.append(highlighter(f" probably sent at {self.timestamp}"))
415
421
 
416
422
  def is_fwded_article(self) -> bool:
417
423
  return bool(self.config and self.config.is_fwded_article)
@@ -419,6 +425,9 @@ class Email(Communication):
419
425
  def is_junk_mail(self) -> bool:
420
426
  return self.author in JUNK_EMAILERS or self.author in MAILING_LISTS
421
427
 
428
+ def is_note_to_self(self) -> bool:
429
+ return self.recipients == [self.author]
430
+
422
431
  def metadata(self) -> Metadata:
423
432
  local_metadata = asdict(self)
424
433
  local_metadata['is_junk_mail'] = self.is_junk_mail()
@@ -438,7 +447,10 @@ class Email(Communication):
438
447
  ], join=', ')
439
448
 
440
449
  def subject(self) -> str:
441
- return self.header.subject or ''
450
+ if self.config and self.config.subject:
451
+ return self.config.subject
452
+ else:
453
+ return self.header.subject or ''
442
454
 
443
455
  def summary(self) -> Text:
444
456
  """One line summary mostly for logging."""
@@ -489,11 +501,8 @@ class Email(Communication):
489
501
 
490
502
  def _border_style(self) -> str:
491
503
  """Color emails from epstein to others with the color for the first recipient."""
492
- if self.author == JEFFREY_EPSTEIN:
493
- if len(self.recipients) == 0 or self.recipients == [None]:
494
- style = self.author_style()
495
- else:
496
- style = get_style_for_name(self.recipients[0])
504
+ if self.author == JEFFREY_EPSTEIN and len(self.recipients) > 0:
505
+ style = get_style_for_name(self.recipients[0])
497
506
  else:
498
507
  style = self.author_style()
499
508
 
@@ -763,7 +772,7 @@ class Email(Communication):
763
772
  if args.whole_file:
764
773
  num_chars = len(self.text)
765
774
  elif self.file_id in TRUNCATION_LENGTHS:
766
- num_chars = TRUNCATION_LENGTHS[self.file_id]
775
+ num_chars = TRUNCATION_LENGTHS[self.file_id] or self.file_size()
767
776
  elif self.author in TRUNCATE_ALL_EMAILS_FROM or includes_truncate_term:
768
777
  num_chars = int(MAX_CHARS_TO_PRINT / 3)
769
778
  elif quote_cutoff and quote_cutoff < MAX_CHARS_TO_PRINT:
@@ -830,26 +839,37 @@ class Email(Communication):
830
839
  self.log_top_lines(self.header.num_header_rows + 4, f'Original header:')
831
840
 
832
841
  @staticmethod
833
- def build_emails_table(emails: list['Email'], _author: str | None, include_title: bool = False) -> Table:
834
- """Turn a set of Emails to/from a given _author into a Table."""
835
- author = _author or UNKNOWN
836
-
837
- table = Table(
838
- title=f"Emails to/from {author} starting {emails[0].timestamp.date()}" if include_title else None,
839
- border_style=get_style_for_name(author, allow_bold=False),
840
- header_style="bold"
842
+ def build_emails_table(emails: list['Email'], author: str | None = '', title: str = '', show_length: bool = False) -> Table:
843
+ """Turn a set of Emails into a Table."""
844
+ if title and author:
845
+ raise ValueError(f"Can't provide both 'author' and 'title' args")
846
+ elif author == '' and title == '':
847
+ raise ValueError(f"Must provide either 'author' or 'title' arg")
848
+
849
+ author_style = get_style_for_name(author, allow_bold=False)
850
+ link_style = author_style if author else ARCHIVE_LINK_COLOR
851
+
852
+ table = build_table(
853
+ title or None,
854
+ cols=[col for col in EMAIL_TABLE_COLS if show_length or col['name'] not in ['Length']],
855
+ border_style=DEFAULT_TABLE_KWARGS['border_style'] if title else author_style,
856
+ header_style="bold",
857
+ highlight=True,
841
858
  )
842
859
 
843
- table.add_column('From', justify='left')
844
- table.add_column('Timestamp', justify='center')
845
- table.add_column('Subject', justify='left', style='honeydew2', min_width=70)
846
-
847
860
  for email in emails:
848
- table.add_row(
861
+ fields = [
862
+ email.epstein_media_link(link_txt=email.timestamp_without_seconds(), style=link_style),
849
863
  email.author_txt(),
850
- email.epstein_media_link(link_txt=email.timestamp_without_seconds()),
851
- highlighter(email.subject())
852
- )
864
+ email.recipients_txt(max_full_names=1),
865
+ f"{email.length()}",
866
+ email.subject(),
867
+ ]
868
+
869
+ if not show_length:
870
+ del fields[3]
871
+
872
+ table.add_row(*fields)
853
873
 
854
874
  return table
855
875
 
@@ -11,19 +11,16 @@ from epstein_files.util.highlighted_group import get_style_for_name
11
11
  from epstein_files.util.logging import logger
12
12
  from epstein_files.util.rich import TEXT_LINK, highlighter
13
13
 
14
+ EPSTEIN_TEXTERS = ['e:', 'e:jeeitunes@gmail.com']
14
15
  MSG_DATE_FORMAT = r"%m/%d/%y %I:%M:%S %p"
15
16
  PHONE_NUMBER_REGEX = re.compile(r'^[\d+]+.*')
17
+ UNCERTAIN_SUFFIX = ' (?)'
16
18
 
17
19
  DISPLAY_LAST_NAME_ONLY = [
18
20
  JEFFREY_EPSTEIN,
19
21
  STEVE_BANNON,
20
22
  ]
21
23
 
22
- TEXTER_MAPPING = {
23
- 'e:': JEFFREY_EPSTEIN,
24
- 'e:jeeitunes@gmail.com': JEFFREY_EPSTEIN,
25
- }
26
-
27
24
 
28
25
  @dataclass(kw_only=True)
29
26
  class TextMessage:
@@ -35,7 +32,7 @@ class TextMessage:
35
32
  timestamp_str: str
36
33
 
37
34
  def __post_init__(self):
38
- self.author = TEXTER_MAPPING.get(self.author or UNKNOWN, self.author)
35
+ self.author = JEFFREY_EPSTEIN if self.author in EPSTEIN_TEXTERS else self.author
39
36
 
40
37
  if not self.author:
41
38
  self.author_str = UNKNOWN
@@ -45,7 +42,7 @@ class TextMessage:
45
42
  self.author_str = self.author_str or self.author
46
43
 
47
44
  if not self.is_id_confirmed and self.author is not None and self.author != JEFFREY_EPSTEIN:
48
- self.author_str += ' (?)'
45
+ self.author_str += UNCERTAIN_SUFFIX
49
46
 
50
47
  if self.is_link():
51
48
  self.text = self.text.replace('\n', '').replace(' ', '_')
@@ -59,12 +56,11 @@ class TextMessage:
59
56
  return datetime.strptime(self.timestamp_str, MSG_DATE_FORMAT)
60
57
 
61
58
  def timestamp_txt(self) -> Text:
62
- timestamp_str = self.timestamp_str
63
-
64
59
  try:
65
60
  timestamp_str = iso_timestamp(self.parse_timestamp())
66
61
  except Exception as e:
67
62
  logger.warning(f"Failed to parse timestamp for {self}")
63
+ timestamp_str = self.timestamp_str
68
64
 
69
65
  return Text(f"[{timestamp_str}]", style=TIMESTAMP_DIM)
70
66
 
@@ -14,7 +14,7 @@ from epstein_files.util.constant.names import JEFFREY_EPSTEIN, UNKNOWN
14
14
  from epstein_files.util.constant.strings import AUTHOR, TIMESTAMP_STYLE
15
15
  from epstein_files.util.data import days_between, days_between_str, iso_timestamp, sort_dict
16
16
  from epstein_files.util.doc_cfg import Metadata, TextCfg
17
- from epstein_files.util.highlighted_group import get_style_for_name
17
+ from epstein_files.util.highlighted_group import get_style_for_name, styled_name
18
18
  from epstein_files.util.logging import logger
19
19
  from epstein_files.util.rich import LAST_TIMESTAMP_STYLE, build_table, highlighter
20
20
 
@@ -160,7 +160,7 @@ class MessengerLog(Communication):
160
160
  last_at = logs[-1].first_message_at(name)
161
161
 
162
162
  counts_table.add_row(
163
- Text(name or UNKNOWN, get_style_for_name(name)),
163
+ styled_name(name),
164
164
  str(len(logs)),
165
165
  f"{count:,}",
166
166
  iso_timestamp(first_at),
@@ -30,7 +30,6 @@ from epstein_files.util.rich import (NA_TXT, add_cols_to_table, build_table, con
30
30
  from epstein_files.util.search_result import SearchResult
31
31
  from epstein_files.util.timer import Timer
32
32
 
33
- EXCLUDED_EMAILERS = USELESS_EMAILERS + [JEFFREY_EPSTEIN]
34
33
  DEVICE_SIGNATURE_SUBTITLE = f"Email [italic]Sent from \\[DEVICE][/italic] Signature Breakdown"
35
34
  DEVICE_SIGNATURE = 'Device Signature'
36
35
  DEVICE_SIGNATURE_PADDING = (1, 0)
@@ -116,9 +115,9 @@ class EpsteinFiles:
116
115
  return self.imessage_logs + self.emails + self.other_files
117
116
 
118
117
  def all_emailers(self, include_useless: bool = False) -> list[str | None]:
119
- """Returns all emailers except Epstein and EXCLUDED_EMAILERS, sorted from least frequent to most."""
118
+ """Returns all emailers USELESS_EMAILERS, sorted from least frequent to most."""
120
119
  names = [a for a in self.email_author_counts.keys()] + [r for r in self.email_recipient_counts.keys()]
121
- names = names if include_useless else [e for e in names if e not in EXCLUDED_EMAILERS]
120
+ names = names if include_useless else [e for e in names if e not in USELESS_EMAILERS]
122
121
  return sorted(list(set(names)), key=lambda e: self.email_author_counts[e] + self.email_recipient_counts[e])
123
122
 
124
123
  def docs_matching(
@@ -167,7 +166,10 @@ class EpsteinFiles:
167
166
 
168
167
  def emails_for(self, author: str | None) -> list[Email]:
169
168
  """Returns emails to or from a given 'author' sorted chronologically."""
170
- emails = self.emails_by(author) + self.emails_to(author)
169
+ if author == JEFFREY_EPSTEIN:
170
+ emails = [e for e in self.emails_by(JEFFREY_EPSTEIN) if e.is_note_to_self()]
171
+ else:
172
+ emails = self.emails_by(author) + self.emails_to(author)
171
173
 
172
174
  if len(emails) == 0:
173
175
  raise RuntimeError(f"No emails found for '{author}'")
@@ -182,7 +184,7 @@ class EpsteinFiles:
182
184
 
183
185
  return Document.sort_by_timestamp(emails)
184
186
 
185
- def get_documents_by_id(self, file_ids: str | list[str]) -> list[Document]:
187
+ def for_ids(self, file_ids: str | list[str]) -> list[Document]:
186
188
  file_ids = listify(file_ids)
187
189
  docs = [doc for doc in self.all_documents() if doc.file_id in file_ids]
188
190
 
@@ -247,13 +249,14 @@ class EpsteinFiles:
247
249
  unique_emails = [email for email in emails if not email.is_duplicate()]
248
250
  start_date = emails[0].timestamp.date()
249
251
  author = _author or UNKNOWN
252
+ title = f"Found {len(unique_emails)} emails"
250
253
 
251
- print_author_panel(
252
- f"Found {len(unique_emails)} emails to/from {author} starting {start_date} covering {num_days:,} days",
253
- get_style_for_name(author),
254
- get_info_for_name(author)
255
- )
254
+ if author == JEFFREY_EPSTEIN:
255
+ title += f" sent by {JEFFREY_EPSTEIN} to himself"
256
+ else:
257
+ title += f" to/from {author} starting {start_date} covering {num_days:,} days"
256
258
 
259
+ print_author_panel(title, get_info_for_name(author), get_style_for_name(author))
257
260
  self.print_emails_table_for(_author)
258
261
  last_printed_email_was_duplicate = False
259
262
 
@@ -272,11 +275,10 @@ class EpsteinFiles:
272
275
 
273
276
  def print_emails_table_for(self, author: str | None) -> None:
274
277
  emails = [email for email in self.emails_for(author) if not email.is_duplicate()] # Remove dupes
275
- print_centered(Email.build_emails_table(emails, author))
276
- console.line()
278
+ print_centered(Padding(Email.build_emails_table(emails, author), (0, 5, 1, 5)))
277
279
 
278
280
  def print_email_device_info(self) -> None:
279
- print_subtitle_panel(DEVICE_SIGNATURE_SUBTITLE, padding=(2, 0, 0, 0), centered=True)
281
+ print_subtitle_panel(DEVICE_SIGNATURE_SUBTITLE)
280
282
  console.print(_build_signature_table(self.email_device_signatures_to_authors, (DEVICE_SIGNATURE, AUTHOR), ', '))
281
283
  console.print(_build_signature_table(self.email_authors_to_device_signatures, (AUTHOR, DEVICE_SIGNATURE)))
282
284
 
@@ -178,6 +178,7 @@ HENRY_HOLT = 'Henry Holt' # Actually a company?
178
178
  IVANKA = 'Ivanka'
179
179
  JAMES_PATTERSON = 'James Patterson'
180
180
  JARED_KUSHNER = 'Jared Kushner'
181
+ JEFFREY_WERNICK = 'Jeffrey Wernick'
181
182
  JULIE_K_BROWN = 'Julie K. Brown'
182
183
  KARIM_SADJADPOUR = 'KARIM SADJADPOUR'.title()
183
184
  MICHAEL_J_BOCCIO = 'Michael J. Boccio'
@@ -206,63 +207,54 @@ ROTHSTEIN_ROSENFELDT_ADLER = 'Rothstein Rosenfeldt Adler (Rothstein was a crook
206
207
  TRUMP_ORG = 'Trump Organization'
207
208
  UBS = 'UBS'
208
209
 
209
- # Invalid for links to EpsteinWeb
210
- JUNK_EMAILERS = [
211
- 'asmallworld@travel.asmallworld.net',
212
- "digest-noreply@quora.com",
213
- 'editorialstaff@flipboard.com',
214
- 'How To Academy',
215
- 'Jokeland',
216
- ]
217
-
218
210
  # First and last names that should be made part of a highlighting regex for emailers
219
- NAMES_TO_NOT_HIGHLIGHT: list[str] = [name.lower() for name in [
220
- 'Al', 'Alan', 'Alfredo', 'Allen', 'Alex', 'Alexander', 'Amanda', 'Andres', 'Andrew',
221
- 'Bard', 'Barry', 'Bill', 'Black', 'Boris', 'Brad', 'Bruce',
222
- 'Carolyn', 'Chris', 'Christina',
223
- 'Dan', 'Daniel', 'Danny', 'Darren', 'Dave', 'David',
224
- 'Ed', 'Edward', 'Edwards', 'Epstein', 'Eric', 'Erika', 'Etienne',
225
- 'Faith', 'Fred', 'Friendly', 'Frost', 'Fuller',
226
- 'George',
227
- 'Heather', 'Henry', 'Hill', 'Hoffman',
228
- 'Ian',
229
- 'Jack', 'James', 'Jay', 'Jean', 'Jeff', 'Jeffrey', 'Jennifer', 'Jeremy', 'jessica', 'Joel', 'John', 'Jon', 'Jonathan', 'Joseph', 'Jr',
230
- 'Kahn', 'Katherine', 'Kelly', 'Ken', 'Kevin',
231
- 'Larry', 'Leon', 'Lesley', 'Linda', 'Link', 'Lisa',
232
- 'Mann', 'Marc', 'Marie', 'Mark', 'Martin', 'Melanie', 'Michael', 'Mike', 'Miller', 'Mitchell', 'Miles', 'Morris', 'Moskowitz',
233
- 'Nancy', 'Neal', 'New',
234
- 'Paul', 'Paula', 'Pen', 'Peter', 'Philip', 'Prince',
235
- 'Randall', 'Reid', 'Richard', 'Robert', 'Rodriguez', 'Roger', 'Rosenberg', 'Ross', 'Roth', 'Rubin',
236
- 'Scott', 'Sean', 'Stanley', 'Stern', 'Stephen', 'Steve', 'Steven', 'Stone', 'Susan',
237
- 'The', 'Thomas', 'Tim', 'Tom', 'Tyler',
238
- 'Victor',
239
- 'Wade',
240
- "Y",
241
- ]]
211
+ NAMES_TO_NOT_HIGHLIGHT = """
212
+ al alan alfredo allen alex alexander amanda andres andrew
213
+ bard barrett barry bill black boris brad bruce
214
+ carolyn chris christina
215
+ dan daniel danny darren dave david donald
216
+ ed edward edwards enterprise enterprises entourage epstein eric erika etienne
217
+ faith fred friendly frost fuller
218
+ gerald george gold
219
+ harry hay heather henry hill hoffman
220
+ ian
221
+ jack james jay jean jeff jeffrey jennifer jeremy jessica joel john jon jonathan joseph jr
222
+ kahn karl kate katherine kelly ken kevin
223
+ larry laurie lawrence leon lesley linda link lisa
224
+ mann marc marie mark martin melanie michael mike miller mitchell miles morris moskowitz
225
+ nancy neal new nicole
226
+ owen
227
+ paul paula pen peter philip prince
228
+ randall reid richard robert rodriguez roger rosenberg ross roth roy rubin
229
+ scott sean skip stanley stern stephen steve steven stone susan
230
+ the thomas tim tom tony tyler
231
+ victor
232
+ wade waters
233
+ y
234
+ """.strip().split()
242
235
 
243
236
  # Names to color white in the word counts
244
237
  OTHER_NAMES = NAMES_TO_NOT_HIGHLIGHT + """
245
238
  aaron albert alberto alec alexandra alice anderson andre ann anna anne ariana arthur
246
239
  baldwin barack barrett ben benjamin berger bert binant bob bonner boyden bradley brady branson bright bruno bryant burton
247
240
  chapman charles charlie christopher clint cohen colin collins conway
248
- danny davis dean debra deborah dennis diana diane diaz dickinson dixon dominique don dylan
249
- edmond elizabeth emily enterprises entwistle erik evelyn
250
- ferguson flachsbart francis franco frank frost
251
- gardner gary geoff geoffrey gerald gilbert gloria gold goldberg gonzalez gould graham greene guarino gwyneth
252
- hancock harold harrison harry hay helen hill hirsch hofstadter horowitz hussein
253
- ian isaac isaacson
254
- james jamie jane janet jason jeffrey jen jim joe johnson jones josh julie justin
255
- karl kate kathy kelly kim kruger kyle
256
- laurie lawrence leo leonard lenny leslie lieberman louis lynch lynn
241
+ davis dean debra deborah dennis diana diane diaz dickinson dixon dominique don dylan
242
+ edmond elizabeth emily entwistle erik evelyn
243
+ ferguson flachsbart francis franco frank
244
+ gardner gary geoff geoffrey gilbert gloria goldberg gonzalez gould graham greene guarino gwyneth
245
+ hancock harold harrison helen hirsch hofstadter horowitz hussein
246
+ isaac isaacson
247
+ jamie jane janet jason jeffrey jen jim joe johnson jones josh julie justin
248
+ kathy kim kruger kyle
249
+ lawrence leo leonard lenny leslie lieberman louis lynch lynn
257
250
  marcus marianne matt matthew melissa michele michelle moore moscowitz
258
- nancy nicole nussbaum
259
- owen
260
- paulson peter philippe
251
+ nancy nussbaum
252
+ paulson philippe
261
253
  rafael ray richard richardson rob robert robin ron rubin rudolph ryan
262
- sara sarah sean seligman serge sergey silverman sloman smith snowden sorkin steele stevie stewart
263
- ted theresa thompson tiffany timothy tony
254
+ sara sarah seligman serge sergey silverman sloman smith snowden sorkin steele stevie stewart
255
+ ted theresa thompson tiffany timothy
264
256
  valeria
265
- walter warren waters weinstein weiss william
257
+ walter warren weinstein weiss william
266
258
  zach zack
267
259
  """.strip().split()
268
260
 
@@ -50,6 +50,7 @@ TEXT_MESSAGE = 'text message'
50
50
  SiteType = Literal['email', 'text message']
51
51
 
52
52
  # Styles
53
+ DEFAULT_NAME_STYLE = 'gray46'
53
54
  TIMESTAMP_STYLE = 'turquoise4'
54
55
  TIMESTAMP_DIM = f"turquoise4 dim"
55
56