mailsort 0.0.1__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.
mailsort/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from mailsort.local import Imap
2
+
3
+ from . import _version
4
+
5
+ __version__: str = _version.__version__
6
+ __all__ = ["Imap"]
mailsort/__main__.py ADDED
@@ -0,0 +1,99 @@
1
+ import argparse
2
+ import os
3
+
4
+ from mailsort import Imap
5
+
6
+
7
+ def command_line_parser():
8
+ """
9
+ Main function primarily used for the command line interface of the IMAP backend
10
+ """
11
+ parser = argparse.ArgumentParser(prog="mailsort")
12
+ parser.add_argument(
13
+ "--host",
14
+ help="IMAP server hostname e.g. imap.example.com .",
15
+ )
16
+ parser.add_argument(
17
+ "--port",
18
+ type=int,
19
+ default=993,
20
+ help="IMAP server port - default: 993 .",
21
+ )
22
+ parser.add_argument(
23
+ "--username",
24
+ help="IMAP account username.",
25
+ )
26
+ parser.add_argument(
27
+ "--password-env",
28
+ default="IMAP_PASSWORD",
29
+ help=(
30
+ "Name of the environment variable holding the IMAP account password - "
31
+ "default: IMAP_PASSWORD ."
32
+ ),
33
+ )
34
+ parser.add_argument(
35
+ "--no-ssl",
36
+ action="store_true",
37
+ help="Connect without SSL (IMAP4 instead of IMAP4_SSL).",
38
+ )
39
+ parser.add_argument(
40
+ "-d",
41
+ "--database",
42
+ help="Connection string to connect to database e.g. sqlite:///email.db .",
43
+ )
44
+ parser.add_argument(
45
+ "-u",
46
+ "--update",
47
+ action="store_true",
48
+ help="Update local database and retrain machine learning model.",
49
+ )
50
+ parser.add_argument(
51
+ "-i",
52
+ "--identification",
53
+ help="User ID of the database user e.g. 1 .",
54
+ )
55
+ parser.add_argument(
56
+ "-l",
57
+ "--label",
58
+ help="Email label (IMAP folder) to be filtered with machine learning.",
59
+ )
60
+ args = parser.parse_args()
61
+ db_user_id = int(args.identification) if args.identification else 1
62
+ password = os.environ.get(args.password_env)
63
+ if not args.host or not args.username:
64
+ print("Please provide --host and --username.")
65
+ elif not password:
66
+ print(
67
+ f"Please set the {args.password_env} environment variable to your IMAP password."
68
+ )
69
+ else:
70
+ database = args.database or "sqlite:///email.db"
71
+ imap = Imap(
72
+ host=args.host,
73
+ port=args.port,
74
+ username=args.username,
75
+ password=password,
76
+ connection_str=database,
77
+ db_user_id=db_user_id,
78
+ use_ssl=not args.no_ssl,
79
+ email_download_format="metadata",
80
+ )
81
+ if args.update:
82
+ imap.update_database(quick=False)
83
+ imap.fit_machine_learning_model_to_database(
84
+ n_estimators=100,
85
+ max_features=400,
86
+ random_state=42,
87
+ bootstrap=True,
88
+ include_deleted=False,
89
+ )
90
+ elif args.label:
91
+ imap.filter_messages_from_server(
92
+ label=args.label, recommendation_ratio=0.9, label_prefix="labels_"
93
+ )
94
+ else:
95
+ parser.print_help()
96
+
97
+
98
+ if __name__ == "__main__":
99
+ command_line_parser()
mailsort/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.0.1'
22
+ __version_tuple__ = version_tuple = (0, 0, 1)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,3 @@
1
+ from mailsort.base.database import get_email_database
2
+
3
+ __all__ = ["get_email_database"]
@@ -0,0 +1,465 @@
1
+ from typing import Any
2
+
3
+ import pandas
4
+ from sqlalchemy import Boolean, Column, DateTime, Engine, ForeignKey, Integer, String
5
+ from sqlalchemy.orm import Session, declarative_base
6
+ from tqdm import tqdm
7
+
8
+ Base = declarative_base()
9
+
10
+
11
+ class EmailContent(Base):
12
+ __tablename__ = "email_content"
13
+ id = Column(Integer, primary_key=True)
14
+ email_id = Column(String)
15
+ email_subject = Column(String)
16
+ email_content = Column(String)
17
+ email_deleted = Column(Boolean)
18
+ email_date = Column(DateTime)
19
+ user_id = Column(Integer)
20
+
21
+
22
+ class Threads(Base):
23
+ __tablename__ = "email_threads"
24
+ id = Column(Integer, primary_key=True)
25
+ email_id = Column(String, ForeignKey("email_content.email_id"))
26
+ thread_id = Column(String)
27
+ user_id = Column(Integer)
28
+
29
+
30
+ class Labels(Base):
31
+ __tablename__ = "email_labels"
32
+ id = Column(Integer, primary_key=True)
33
+ email_id = Column(String, ForeignKey("email_content.email_id"))
34
+ label_id = Column(String)
35
+ user_id = Column(Integer)
36
+
37
+
38
+ class EmailTo(Base):
39
+ __tablename__ = "email_to"
40
+ id = Column(Integer, primary_key=True)
41
+ email_id = Column(String, ForeignKey("email_content.email_id"))
42
+ email_to = Column(String)
43
+ user_id = Column(Integer)
44
+
45
+
46
+ class EmailCc(Base):
47
+ __tablename__ = "email_cc"
48
+ id = Column(Integer, primary_key=True)
49
+ email_id = Column(String, ForeignKey("email_content.email_id"))
50
+ email_cc = Column(String)
51
+ user_id = Column(Integer)
52
+
53
+
54
+ class EmailFrom(Base):
55
+ __tablename__ = "email_from"
56
+ id = Column(Integer, primary_key=True)
57
+ email_id = Column(String, ForeignKey("email_content.email_id"))
58
+ email_from = Column(String)
59
+ user_id = Column(Integer)
60
+
61
+
62
+ class DatabaseTemplate:
63
+ def __init__(self, session: Session) -> None:
64
+ self._session = session
65
+
66
+ def close(self) -> None:
67
+ self._session.close()
68
+
69
+
70
+ class DatabaseInterface(DatabaseTemplate):
71
+ @property
72
+ def session(self) -> Session:
73
+ return self._session
74
+
75
+ def store_dataframe(self, df: pandas.DataFrame, user_id: int = 1) -> None:
76
+ self._commit_content_table(df=df, user_id=user_id)
77
+ self._commit_email_from_table(df=df, user_id=user_id)
78
+ self._commit_email_to_table(df=df, user_id=user_id)
79
+ self._commit_email_cc_table(df=df, user_id=user_id)
80
+ self._commit_label_table(df=df, user_id=user_id)
81
+ self._commit_thread_table(df=df, user_id=user_id)
82
+
83
+ def list_email_ids(self, user_id: int = 1) -> list[str]:
84
+ return [
85
+ instance.email_id
86
+ for instance in self._session.query(EmailContent)
87
+ .filter(EmailContent.user_id == user_id)
88
+ .order_by(EmailContent.id)
89
+ ]
90
+
91
+ def mark_emails_as_deleted(
92
+ self, message_id_lst: list[str], user_id: int = 1
93
+ ) -> None:
94
+ for instance in (
95
+ self._session.query(EmailContent)
96
+ .filter(EmailContent.user_id == user_id)
97
+ .filter(EmailContent.email_id.in_(message_id_lst))
98
+ .all()
99
+ ):
100
+ instance.email_deleted = True
101
+ self._session.commit()
102
+
103
+ def get_labels_to_update(
104
+ self, message_id_lst: list[str], user_id: int = 1
105
+ ) -> tuple[list[str], list[str], list[str]]:
106
+ email_in_db_id = self.list_email_ids(user_id=user_id)
107
+ new_messages_lst = [m for m in message_id_lst if m not in email_in_db_id]
108
+ deleted_messages_lst = [m for m in email_in_db_id if m not in message_id_lst]
109
+ message_label_updates_lst = [m for m in message_id_lst if m in email_in_db_id]
110
+ return new_messages_lst, message_label_updates_lst, deleted_messages_lst
111
+
112
+ def update_labels(
113
+ self,
114
+ message_id_lst: list[str],
115
+ message_meta_lst: list[list[str]],
116
+ user_id: int = 1,
117
+ ) -> None:
118
+ for message_id, message_labels in tqdm(
119
+ iterable=zip(message_id_lst, message_meta_lst, strict=False),
120
+ desc="Update labels",
121
+ total=len(message_id_lst),
122
+ ):
123
+ message_label_stored = [
124
+ m
125
+ for (m,) in self._session.query(Labels.label_id)
126
+ .filter(Labels.user_id == user_id)
127
+ .filter(Labels.email_id == message_id)
128
+ .all()
129
+ ]
130
+ if message_label_stored == message_labels:
131
+ continue
132
+ else:
133
+ message_label_stored_set = set(message_label_stored)
134
+ message_labels_set = set(message_labels)
135
+ labels_to_add = list(
136
+ message_labels_set.difference(message_label_stored_set)
137
+ )
138
+ labels_to_remove = list(
139
+ message_label_stored_set.difference(message_labels_set)
140
+ )
141
+ if len(labels_to_add) > 0:
142
+ self._session.add_all(
143
+ [
144
+ Labels(
145
+ email_id=message_id,
146
+ label_id=label_id,
147
+ user_id=user_id,
148
+ )
149
+ for label_id in labels_to_add
150
+ ]
151
+ )
152
+ if len(labels_to_remove) > 0:
153
+ _ = [
154
+ self._session.query(Labels)
155
+ .filter(Labels.user_id == user_id)
156
+ .filter(Labels.email_id == message_id)
157
+ .filter(Labels.label_id == label_id)
158
+ .delete()
159
+ for label_id in labels_to_remove
160
+ ]
161
+ self._session.commit()
162
+
163
+ def get_all_emails(
164
+ self, include_deleted: bool = False, user_id: int = 1
165
+ ) -> pandas.DataFrame:
166
+ if include_deleted:
167
+ email_collect_lst = [
168
+ [
169
+ email.email_id,
170
+ email.email_subject,
171
+ email.email_content,
172
+ email.email_date,
173
+ ]
174
+ for email in self._session.query(EmailContent)
175
+ .filter(EmailContent.user_id == user_id)
176
+ .all()
177
+ ]
178
+ else:
179
+ email_collect_lst = [
180
+ [
181
+ email.email_id,
182
+ email.email_subject,
183
+ email.email_content,
184
+ email.email_date,
185
+ ]
186
+ for email in self._session.query(EmailContent)
187
+ .filter(EmailContent.user_id == user_id)
188
+ .filter(EmailContent.email_deleted.is_(False))
189
+ .all()
190
+ ]
191
+ return self._create_dataframe(
192
+ email_collect_lst=email_collect_lst,
193
+ user_id=user_id,
194
+ desc="Create dataframe from database",
195
+ )
196
+
197
+ def get_emails_by_label(
198
+ self, label_id: str, include_deleted: bool = False, user_id: int = 1
199
+ ) -> pandas.DataFrame:
200
+ return self.get_email_collection(
201
+ email_id_lst=[
202
+ email_id
203
+ for (email_id,) in self._session.query(Labels.email_id)
204
+ .filter(Labels.user_id == user_id)
205
+ .filter(Labels.label_id == label_id)
206
+ .all()
207
+ ],
208
+ include_deleted=include_deleted,
209
+ user_id=user_id,
210
+ desc="Create dataframe from emails by label",
211
+ )
212
+
213
+ def get_emails_by_from(
214
+ self, email_from: str, include_deleted: bool = False, user_id: int = 1
215
+ ) -> pandas.DataFrame:
216
+ return self.get_email_collection(
217
+ email_id_lst=[
218
+ email_id
219
+ for (email_id,) in self._session.query(EmailFrom.email_id)
220
+ .filter(EmailFrom.user_id == user_id)
221
+ .filter(EmailFrom.email_from == email_from)
222
+ .all()
223
+ ],
224
+ include_deleted=include_deleted,
225
+ user_id=user_id,
226
+ desc="Create dataframe from emails by from",
227
+ )
228
+
229
+ def get_emails_by_to(
230
+ self, email_to: str, include_deleted: bool = False, user_id: int = 1
231
+ ) -> pandas.DataFrame:
232
+ return self.get_email_collection(
233
+ email_id_lst=[
234
+ email_id
235
+ for (email_id,) in self._session.query(EmailTo.email_id)
236
+ .filter(EmailTo.user_id == user_id)
237
+ .filter(EmailTo.email_to == email_to)
238
+ .all()
239
+ ],
240
+ include_deleted=include_deleted,
241
+ user_id=user_id,
242
+ desc="Create dataframe from emails by to",
243
+ )
244
+
245
+ def get_emails_by_cc(
246
+ self, email_cc: str, include_deleted: bool = False, user_id: int = 1
247
+ ) -> pandas.DataFrame:
248
+ return self.get_email_collection(
249
+ email_id_lst=[
250
+ email_id
251
+ for (email_id,) in self._session.query(EmailTo.email_id)
252
+ .filter(EmailCc.user_id == user_id)
253
+ .filter(EmailCc.email_cc == email_cc)
254
+ .all()
255
+ ],
256
+ include_deleted=include_deleted,
257
+ user_id=user_id,
258
+ desc="Create dataframe from emails by cc",
259
+ )
260
+
261
+ def get_emails_by_thread(
262
+ self, thread_id: str, include_deleted: bool = False, user_id: int = 1
263
+ ) -> pandas.DataFrame:
264
+ return self.get_email_collection(
265
+ email_id_lst=[
266
+ email_id
267
+ for (email_id,) in self._session.query(Threads.email_id)
268
+ .filter(Threads.user_id == user_id)
269
+ .filter(Threads.thread_id == thread_id)
270
+ .all()
271
+ ],
272
+ include_deleted=include_deleted,
273
+ user_id=user_id,
274
+ desc="Create dataframe from emails by thread",
275
+ )
276
+
277
+ def get_email_collection(
278
+ self,
279
+ email_id_lst: list[str],
280
+ include_deleted: bool = False,
281
+ user_id: int = 1,
282
+ desc: str = "Create dataframe from email collection",
283
+ ) -> pandas.DataFrame:
284
+ if include_deleted:
285
+ email_collect_lst = [
286
+ [
287
+ email.email_id,
288
+ email.email_subject,
289
+ email.email_content,
290
+ email.email_date,
291
+ ]
292
+ for email in self._session.query(EmailContent)
293
+ .filter(EmailContent.user_id == user_id)
294
+ .filter(EmailContent.email_id.in_(email_id_lst))
295
+ .all()
296
+ ]
297
+ else:
298
+ email_collect_lst = [
299
+ [
300
+ email.email_id,
301
+ email.email_subject,
302
+ email.email_content,
303
+ email.email_date,
304
+ ]
305
+ for email in self._session.query(EmailContent)
306
+ .filter(EmailContent.user_id == user_id)
307
+ .filter(EmailContent.email_id.in_(email_id_lst))
308
+ .filter(EmailContent.email_deleted.is_(False))
309
+ .all()
310
+ ]
311
+ return self._create_dataframe(
312
+ email_collect_lst=email_collect_lst, user_id=user_id, desc=desc
313
+ )
314
+
315
+ def _commit_thread_table(self, df: pandas.DataFrame, user_id: int = 1) -> None:
316
+ self._session.add_all(
317
+ [
318
+ Threads(email_id=email_id, thread_id=thread_id, user_id=user_id)
319
+ for email_id, thread_id in zip(df["id"], df["threads"], strict=False)
320
+ ]
321
+ )
322
+ self._session.commit()
323
+
324
+ def _commit_email_from_table(self, df: pandas.DataFrame, user_id: int = 1) -> None:
325
+ self._session.add_all(
326
+ [
327
+ EmailFrom(email_id=email_id, email_from=email_from, user_id=user_id)
328
+ for email_id, email_from in zip(df["id"], df["from"], strict=False)
329
+ ]
330
+ )
331
+ self._session.commit()
332
+
333
+ def _commit_label_table(self, df: pandas.DataFrame, user_id: int = 1) -> None:
334
+ label_lst = []
335
+ for email_id, lid_lst in zip(df["id"], df["labels"], strict=False):
336
+ for label_id in lid_lst:
337
+ label_lst.append(
338
+ Labels(email_id=email_id, label_id=label_id, user_id=user_id)
339
+ )
340
+ self._session.add_all(label_lst)
341
+ self._session.commit()
342
+
343
+ def _commit_email_to_table(self, df: pandas.DataFrame, user_id: int = 1) -> None:
344
+ email_to_lst = []
345
+ for email_id, email_lst in zip(df["id"], df["to"], strict=False):
346
+ for email_to in email_lst:
347
+ email_to_lst.append(
348
+ EmailTo(email_id=email_id, email_to=email_to, user_id=user_id)
349
+ )
350
+ self._session.add_all(email_to_lst)
351
+ self._session.commit()
352
+
353
+ def _commit_email_cc_table(self, df: pandas.DataFrame, user_id: int = 1) -> None:
354
+ email_cc_lst = []
355
+ for email_id, email_lst in zip(df["id"], df["cc"], strict=False):
356
+ for email_cc in email_lst:
357
+ email_cc_lst.append(
358
+ EmailCc(email_id=email_id, email_cc=email_cc, user_id=user_id)
359
+ )
360
+ self._session.add_all(email_cc_lst)
361
+ self._session.commit()
362
+
363
+ def _commit_content_table(self, df: pandas.DataFrame, user_id: int = 1) -> None:
364
+ self._session.add_all(
365
+ [
366
+ EmailContent(
367
+ email_id=email_id,
368
+ email_subject=email_subject,
369
+ email_content=email_content,
370
+ email_deleted=False,
371
+ email_date=email_date,
372
+ user_id=user_id,
373
+ )
374
+ for email_id, email_subject, email_content, email_date in zip(
375
+ df["id"], df["subject"], df["content"], df["date"], strict=False
376
+ )
377
+ ]
378
+ )
379
+ self._session.commit()
380
+
381
+ def _create_dataframe(
382
+ self,
383
+ email_collect_lst: list[list[Any]],
384
+ user_id: int = 1,
385
+ desc: str = "Create dataframe from email list",
386
+ ) -> pandas.DataFrame:
387
+ (
388
+ email_id_lst,
389
+ email_subject_lst,
390
+ email_content_lst,
391
+ email_from_lst,
392
+ email_to_lst,
393
+ email_cc_lst,
394
+ email_threads_lst,
395
+ email_labels_lst,
396
+ email_date_lst,
397
+ ) = ([], [], [], [], [], [], [], [], [])
398
+ for email_id, email_subject, email_content, email_date in tqdm(
399
+ iterable=email_collect_lst, desc=desc
400
+ ):
401
+ email_from = [
402
+ email_from.email_from
403
+ for email_from in self._session.query(EmailFrom)
404
+ .filter(EmailFrom.user_id == user_id)
405
+ .filter(EmailFrom.email_id == email_id)
406
+ .all()
407
+ ]
408
+ email_to = [
409
+ email_to.email_to
410
+ for email_to in self._session.query(EmailTo)
411
+ .filter(EmailTo.user_id == user_id)
412
+ .filter(EmailTo.email_id == email_id)
413
+ .all()
414
+ ]
415
+ email_cc = [
416
+ email_cc.email_cc
417
+ for email_cc in self._session.query(EmailCc)
418
+ .filter(EmailCc.user_id == user_id)
419
+ .filter(EmailCc.email_id == email_id)
420
+ .all()
421
+ ]
422
+ label_lst = [
423
+ labels.label_id
424
+ for labels in self._session.query(Labels)
425
+ .filter(Labels.user_id == user_id)
426
+ .filter(Labels.email_id == email_id)
427
+ .all()
428
+ ]
429
+ thread_lst = [
430
+ threads.thread_id
431
+ for threads in self._session.query(Threads)
432
+ .filter(Threads.user_id == user_id)
433
+ .filter(Threads.email_id == email_id)
434
+ .all()
435
+ ]
436
+ if len(email_from) > 0:
437
+ email_from_lst.append(email_from[0])
438
+ else:
439
+ email_from_lst.append(None)
440
+ email_cc_lst.append(email_cc)
441
+ email_to_lst.append(email_to)
442
+ email_labels_lst.append(label_lst)
443
+ email_threads_lst.append(thread_lst[0])
444
+ email_id_lst.append(email_id)
445
+ email_subject_lst.append(email_subject)
446
+ email_content_lst.append(email_content)
447
+ email_date_lst.append(email_date)
448
+ return pandas.DataFrame(
449
+ {
450
+ "id": email_id_lst,
451
+ "from": email_from_lst,
452
+ "to": email_to_lst,
453
+ "cc": email_cc_lst,
454
+ "date": email_date_lst,
455
+ "threads": email_threads_lst,
456
+ "labels": email_labels_lst,
457
+ "subject": email_subject_lst,
458
+ "content": email_content_lst,
459
+ }
460
+ )
461
+
462
+
463
+ def get_email_database(engine: Engine, session: Session) -> DatabaseInterface:
464
+ Base.metadata.create_all(engine)
465
+ return DatabaseInterface(session=session)