pygpt-net 2.4.41__py3-none-any.whl → 2.4.43__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 (146) hide show
  1. CHANGELOG.md +13 -0
  2. README.md +142 -70
  3. pygpt_net/CHANGELOG.txt +13 -0
  4. pygpt_net/__init__.py +3 -3
  5. pygpt_net/app.py +3 -1
  6. pygpt_net/controller/audio/__init__.py +2 -2
  7. pygpt_net/controller/camera.py +1 -10
  8. pygpt_net/controller/chat/attachment.py +36 -23
  9. pygpt_net/controller/chat/audio.py +2 -2
  10. pygpt_net/controller/config/placeholder.py +15 -1
  11. pygpt_net/controller/ui/mode.py +16 -21
  12. pygpt_net/core/attachments/__init__.py +1 -1
  13. pygpt_net/core/attachments/context.py +10 -8
  14. pygpt_net/core/audio/__init__.py +4 -1
  15. pygpt_net/core/audio/whisper.py +37 -0
  16. pygpt_net/core/bridge/worker.py +2 -2
  17. pygpt_net/core/db/__init__.py +2 -1
  18. pygpt_net/core/debug/events.py +22 -10
  19. pygpt_net/core/debug/tabs.py +6 -3
  20. pygpt_net/core/history.py +3 -2
  21. pygpt_net/core/idx/__init__.py +16 -4
  22. pygpt_net/core/idx/chat.py +15 -5
  23. pygpt_net/core/idx/indexing.py +24 -8
  24. pygpt_net/core/installer.py +2 -4
  25. pygpt_net/core/models.py +62 -17
  26. pygpt_net/core/modes.py +11 -13
  27. pygpt_net/core/notepad.py +4 -4
  28. pygpt_net/core/plugins.py +27 -16
  29. pygpt_net/core/presets.py +20 -9
  30. pygpt_net/core/profile.py +11 -3
  31. pygpt_net/core/render/web/parser.py +3 -1
  32. pygpt_net/core/settings.py +5 -5
  33. pygpt_net/core/tabs/tab.py +10 -2
  34. pygpt_net/core/tokens.py +8 -6
  35. pygpt_net/core/web/__init__.py +105 -0
  36. pygpt_net/core/{web.py → web/helpers.py} +93 -67
  37. pygpt_net/data/config/config.json +3 -3
  38. pygpt_net/data/config/models.json +3 -3
  39. pygpt_net/data/config/modes.json +3 -3
  40. pygpt_net/data/locale/locale.en.ini +1 -0
  41. pygpt_net/data/locale/plugin.cmd_web.en.ini +2 -0
  42. pygpt_net/data/locale/plugin.mailer.en.ini +21 -0
  43. pygpt_net/item/ctx.py +66 -3
  44. pygpt_net/migrations/Version20241215110000.py +25 -0
  45. pygpt_net/migrations/__init__.py +3 -1
  46. pygpt_net/plugin/agent/__init__.py +7 -2
  47. pygpt_net/plugin/audio_output/__init__.py +6 -1
  48. pygpt_net/plugin/base/plugin.py +58 -26
  49. pygpt_net/plugin/base/worker.py +20 -17
  50. pygpt_net/plugin/cmd_history/config.py +2 -2
  51. pygpt_net/plugin/cmd_web/__init__.py +3 -4
  52. pygpt_net/plugin/cmd_web/config.py +71 -3
  53. pygpt_net/plugin/cmd_web/websearch.py +20 -12
  54. pygpt_net/plugin/cmd_web/worker.py +67 -4
  55. pygpt_net/plugin/idx_llama_index/config.py +3 -3
  56. pygpt_net/plugin/mailer/__init__.py +123 -0
  57. pygpt_net/plugin/mailer/config.py +149 -0
  58. pygpt_net/plugin/mailer/runner.py +285 -0
  59. pygpt_net/plugin/mailer/worker.py +123 -0
  60. pygpt_net/provider/agents/base.py +5 -2
  61. pygpt_net/provider/agents/openai.py +4 -2
  62. pygpt_net/provider/agents/openai_assistant.py +4 -2
  63. pygpt_net/provider/agents/planner.py +4 -2
  64. pygpt_net/provider/agents/react.py +4 -2
  65. pygpt_net/provider/audio_output/openai_tts.py +5 -11
  66. pygpt_net/provider/core/assistant/base.py +5 -3
  67. pygpt_net/provider/core/assistant/json_file.py +8 -5
  68. pygpt_net/provider/core/assistant_file/base.py +4 -3
  69. pygpt_net/provider/core/assistant_file/db_sqlite/__init__.py +4 -3
  70. pygpt_net/provider/core/assistant_file/db_sqlite/storage.py +3 -2
  71. pygpt_net/provider/core/assistant_store/base.py +6 -4
  72. pygpt_net/provider/core/assistant_store/db_sqlite/__init__.py +5 -4
  73. pygpt_net/provider/core/assistant_store/db_sqlite/storage.py +5 -3
  74. pygpt_net/provider/core/attachment/base.py +5 -3
  75. pygpt_net/provider/core/attachment/json_file.py +4 -3
  76. pygpt_net/provider/core/calendar/base.py +5 -3
  77. pygpt_net/provider/core/calendar/db_sqlite/__init__.py +6 -5
  78. pygpt_net/provider/core/calendar/db_sqlite/storage.py +5 -4
  79. pygpt_net/provider/core/config/base.py +8 -6
  80. pygpt_net/provider/core/config/json_file.py +9 -7
  81. pygpt_net/provider/core/ctx/base.py +27 -25
  82. pygpt_net/provider/core/ctx/db_sqlite/__init__.py +51 -35
  83. pygpt_net/provider/core/ctx/db_sqlite/storage.py +92 -38
  84. pygpt_net/provider/core/ctx/db_sqlite/utils.py +37 -11
  85. pygpt_net/provider/core/index/base.py +129 -23
  86. pygpt_net/provider/core/index/db_sqlite/__init__.py +130 -23
  87. pygpt_net/provider/core/index/db_sqlite/storage.py +130 -23
  88. pygpt_net/provider/core/index/db_sqlite/utils.py +4 -2
  89. pygpt_net/provider/core/mode/base.py +5 -3
  90. pygpt_net/provider/core/mode/json_file.py +7 -6
  91. pygpt_net/provider/core/model/base.py +6 -4
  92. pygpt_net/provider/core/model/json_file.py +9 -7
  93. pygpt_net/provider/core/notepad/base.py +5 -3
  94. pygpt_net/provider/core/notepad/db_sqlite/__init__.py +5 -4
  95. pygpt_net/provider/core/notepad/db_sqlite/storage.py +4 -3
  96. pygpt_net/provider/core/plugin_preset/base.py +4 -2
  97. pygpt_net/provider/core/plugin_preset/json_file.py +5 -3
  98. pygpt_net/provider/core/preset/base.py +6 -4
  99. pygpt_net/provider/core/preset/json_file.py +9 -9
  100. pygpt_net/provider/core/prompt/base.py +6 -3
  101. pygpt_net/provider/core/prompt/json_file.py +11 -6
  102. pygpt_net/provider/gpt/assistants.py +15 -6
  103. pygpt_net/provider/gpt/audio.py +5 -5
  104. pygpt_net/provider/gpt/chat.py +7 -5
  105. pygpt_net/provider/gpt/completion.py +8 -4
  106. pygpt_net/provider/gpt/image.py +3 -3
  107. pygpt_net/provider/gpt/store.py +46 -12
  108. pygpt_net/provider/gpt/vision.py +16 -11
  109. pygpt_net/provider/llms/anthropic.py +7 -2
  110. pygpt_net/provider/llms/azure_openai.py +26 -5
  111. pygpt_net/provider/llms/base.py +47 -9
  112. pygpt_net/provider/llms/google.py +7 -2
  113. pygpt_net/provider/llms/hugging_face.py +13 -3
  114. pygpt_net/provider/llms/hugging_face_api.py +18 -4
  115. pygpt_net/provider/llms/local.py +7 -2
  116. pygpt_net/provider/llms/ollama.py +30 -6
  117. pygpt_net/provider/llms/openai.py +32 -6
  118. pygpt_net/provider/vector_stores/__init__.py +45 -14
  119. pygpt_net/provider/vector_stores/base.py +35 -8
  120. pygpt_net/provider/vector_stores/chroma.py +13 -3
  121. pygpt_net/provider/vector_stores/ctx_attachment.py +32 -13
  122. pygpt_net/provider/vector_stores/elasticsearch.py +12 -3
  123. pygpt_net/provider/vector_stores/pinecode.py +12 -3
  124. pygpt_net/provider/vector_stores/redis.py +12 -3
  125. pygpt_net/provider/vector_stores/simple.py +12 -3
  126. pygpt_net/provider/vector_stores/temp.py +16 -4
  127. pygpt_net/provider/web/base.py +10 -3
  128. pygpt_net/provider/web/google_custom_search.py +9 -3
  129. pygpt_net/provider/web/microsoft_bing.py +9 -3
  130. pygpt_net/tools/__init__.py +13 -5
  131. pygpt_net/tools/audio_transcriber/__init__.py +4 -3
  132. pygpt_net/tools/base.py +15 -8
  133. pygpt_net/tools/code_interpreter/__init__.py +4 -3
  134. pygpt_net/tools/html_canvas/__init__.py +4 -3
  135. pygpt_net/tools/image_viewer/__init__.py +10 -4
  136. pygpt_net/tools/indexer/__init__.py +8 -7
  137. pygpt_net/tools/media_player/__init__.py +4 -3
  138. pygpt_net/tools/text_editor/__init__.py +36 -10
  139. pygpt_net/ui/layout/chat/output.py +2 -2
  140. pygpt_net/ui/layout/ctx/ctx_list.py +1 -1
  141. pygpt_net/ui/menu/audio.py +12 -1
  142. {pygpt_net-2.4.41.dist-info → pygpt_net-2.4.43.dist-info}/METADATA +143 -71
  143. {pygpt_net-2.4.41.dist-info → pygpt_net-2.4.43.dist-info}/RECORD +146 -138
  144. {pygpt_net-2.4.41.dist-info → pygpt_net-2.4.43.dist-info}/LICENSE +0 -0
  145. {pygpt_net-2.4.41.dist-info → pygpt_net-2.4.43.dist-info}/WHEEL +0 -0
  146. {pygpt_net-2.4.41.dist-info → pygpt_net-2.4.43.dist-info}/entry_points.txt +0 -0
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.12.13 08:00:00 #
9
+ # Updated Date: 2024.12.15 01:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -64,6 +64,9 @@ class Worker(BaseWorker):
64
64
  elif item["cmd"] == "web_extract_images":
65
65
  response = self.cmd_web_extract_images(item)
66
66
 
67
+ elif item["cmd"] == "web_request":
68
+ response = self.cmd_web_request(item)
69
+
67
70
  if response:
68
71
  responses.append(response)
69
72
 
@@ -363,7 +366,7 @@ class Worker(BaseWorker):
363
366
  url = self.get_param(item, "url")
364
367
  if not url:
365
368
  return self.make_response(item, "No URL provided")
366
- links = self.plugin.window.core.web.get_links(url)
369
+ links = self.plugin.window.core.web.helpers.get_links(url)
367
370
  result = {
368
371
  'links': links,
369
372
  }
@@ -385,17 +388,77 @@ class Worker(BaseWorker):
385
388
  download = bool(self.get_param(item, "download"))
386
389
  if not url:
387
390
  return self.make_response(item, "No URL provided")
388
- images = self.plugin.window.core.web.get_images(url)
391
+ images = self.plugin.window.core.web.helpers.get_images(url)
389
392
  result = {
390
393
  'images': images,
391
394
  }
392
395
  if images and download:
393
396
  for img in images:
394
397
  try:
395
- path = self.plugin.window.core.web.download_image(img)
398
+ path = self.plugin.window.core.web.helpers.download_image(img)
396
399
  if path:
397
400
  self.ctx.images_before.append(path)
398
401
  except Exception as e:
399
402
  print(e)
400
403
  self.ctx.urls_before.append(url)
401
404
  return self.make_response(item, result)
405
+
406
+ def cmd_web_request(self, item: dict) -> dict:
407
+ """
408
+ Web request command
409
+
410
+ :param item: command item
411
+ :return: response item
412
+ """
413
+ url = ""
414
+ method = "GET"
415
+ data = None
416
+ raw = None
417
+ json = None
418
+ headers = None
419
+ params = None
420
+ cookies = None
421
+ files = None
422
+
423
+ if self.has_param(item, "url"):
424
+ url = self.get_param(item, "url")
425
+ if not url:
426
+ return self.make_response(item, "No URL provided")
427
+ if self.has_param(item, "method"):
428
+ method = self.get_param(item, "method")
429
+ if self.has_param(item, "data_form"):
430
+ data = self.get_param(item, "data_form")
431
+ if self.has_param(item, "data"):
432
+ raw = self.get_param(item, "data")
433
+ if self.has_param(item, "data_json"):
434
+ json = self.get_param(item, "data_json")
435
+ if self.has_param(item, "headers"):
436
+ headers = self.get_param(item, "headers")
437
+ if self.has_param(item, "params"):
438
+ params = self.get_param(item, "params")
439
+ if self.has_param(item, "cookies"):
440
+ cookies = self.get_param(item, "cookies")
441
+ if self.has_param(item, "files"):
442
+ files = self.get_param(item, "files")
443
+
444
+ code, response = self.window.core.web.helpers.request(
445
+ url=url,
446
+ method=method,
447
+ headers=headers,
448
+ params=params,
449
+ data=data if data else raw,
450
+ json=json,
451
+ cookies=cookies,
452
+ files=files,
453
+ timeout=self.plugin.get_option_value("timeout"),
454
+ disable_ssl_verify=self.plugin.get_option_value("disable_ssl"),
455
+ allow_redirects=True,
456
+ stream=False,
457
+ user_agent=self.plugin.get_option_value("user_agent"))
458
+ result = {
459
+ 'url': url,
460
+ 'code': code,
461
+ 'response': response,
462
+ }
463
+ self.ctx.urls_before.append(url)
464
+ return self.make_response(item, result)
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.11.18 19:00:00 #
9
+ # Updated Date: 2024.12.14 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from pygpt_net.plugin.base.config import BaseConfig, BasePlugin
@@ -71,7 +71,7 @@ class Config(BaseConfig):
71
71
  "model_prepare_question",
72
72
  type="combo",
73
73
  use="models",
74
- value="gpt-3.5-turbo",
74
+ value="gpt-4o-mini",
75
75
  label="Model for question preparation",
76
76
  description="Model used to prepare question before asking Llama-index, default: gpt-3.5-turbo",
77
77
  tooltip="Model",
@@ -88,7 +88,7 @@ class Config(BaseConfig):
88
88
  plugin.add_option(
89
89
  "model_query",
90
90
  type="combo",
91
- value="gpt-3.5-turbo",
91
+ value="gpt-4o-mini",
92
92
  label="Model",
93
93
  description="Model used for querying Llama-index, default: gpt-3.5-turbo",
94
94
  tooltip="Query model",
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2024.12.15 04:00:00 #
10
+ # ================================================== #
11
+
12
+ from pygpt_net.plugin.base.plugin import BasePlugin
13
+ from pygpt_net.core.events import Event
14
+ from pygpt_net.item.ctx import CtxItem
15
+
16
+ from .config import Config
17
+ from .runner import Runner
18
+ from .worker import Worker
19
+
20
+ from pygpt_net.utils import trans
21
+
22
+
23
+ class Plugin(BasePlugin):
24
+ def __init__(self, *args, **kwargs):
25
+ super(Plugin, self).__init__(*args, **kwargs)
26
+ self.id = "mailer"
27
+ self.name = "Mailer"
28
+ self.description = "Provides email sending"
29
+ self.prefix = "Email"
30
+ self.type = [
31
+ 'email',
32
+ ]
33
+ self.order = 100
34
+ self.allowed_cmds = [
35
+ "send_mail",
36
+ "get_emails",
37
+ "get_email_body",
38
+ ]
39
+ self.use_locale = True
40
+ self.runner = Runner(self)
41
+ self.worker = None
42
+ self.config = Config(self)
43
+ self.init_options()
44
+
45
+ def init_options(self):
46
+ """Initialize options"""
47
+ self.config.from_defaults(self)
48
+
49
+ def handle(self, event: Event, *args, **kwargs):
50
+ """
51
+ Handle dispatched event
52
+
53
+ :param event: event object
54
+ :param args: args
55
+ :param kwargs: kwargs
56
+ """
57
+ name = event.name
58
+ data = event.data
59
+ ctx = event.ctx
60
+ silent = data.get("silent", False)
61
+
62
+ if name == Event.CMD_SYNTAX:
63
+ self.cmd_syntax(data)
64
+
65
+ elif name == Event.CMD_EXECUTE:
66
+ self.cmd(
67
+ ctx,
68
+ data['commands'],
69
+ silent,
70
+ )
71
+
72
+ def cmd_syntax(self, data: dict):
73
+ """
74
+ Event: CMD_SYNTAX
75
+
76
+ :param data: event data dict
77
+ """
78
+ for item in self.allowed_cmds:
79
+ if self.has_cmd(item):
80
+ cmd = self.get_cmd(item)
81
+ data['cmd'].append(cmd) # append command
82
+
83
+ def cmd(self, ctx: CtxItem, cmds: list, silent: bool = False):
84
+ """
85
+ Event: CMD_EXECUTE
86
+
87
+ :param ctx: CtxItem
88
+ :param cmds: commands dict
89
+ :param silent: silent mode
90
+ """
91
+ is_cmd = False
92
+ force = False
93
+ my_commands = []
94
+ for item in cmds:
95
+ if item["cmd"] in self.allowed_cmds:
96
+ my_commands.append(item)
97
+ is_cmd = True
98
+ if "force" in item and item["force"]:
99
+ force = True # call from tool
100
+
101
+ if not is_cmd:
102
+ return
103
+
104
+ # set state: busy
105
+ if not silent:
106
+ self.cmd_prepare(ctx, my_commands)
107
+
108
+ try:
109
+ worker = Worker()
110
+ worker.from_defaults(self)
111
+ worker.cmds = my_commands
112
+ worker.ctx = ctx
113
+
114
+ # connect signals
115
+ self.runner.attach_signals(worker.signals)
116
+
117
+ if not self.is_async(ctx) and not force:
118
+ worker.run()
119
+ return
120
+ worker.run_async()
121
+
122
+ except Exception as e:
123
+ self.error(e)
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2024.12.15 04:00:00 #
10
+ # ================================================== #
11
+
12
+ from pygpt_net.plugin.base.config import BaseConfig, BasePlugin
13
+
14
+
15
+ class Config(BaseConfig):
16
+ def __init__(self, plugin: BasePlugin = None, *args, **kwargs):
17
+ super(Config, self).__init__(plugin)
18
+ self.plugin = plugin
19
+
20
+ def from_defaults(self, plugin: BasePlugin = None):
21
+ """
22
+ Set default options for plugin
23
+
24
+ :param plugin: plugin instance
25
+ """
26
+ plugin.add_option(
27
+ "smtp_host",
28
+ type="text",
29
+ value="",
30
+ label="SMTP Host",
31
+ persist=True,
32
+ tab="smtp",
33
+ )
34
+ plugin.add_option(
35
+ "smtp_port_inbox",
36
+ type="int",
37
+ value=995,
38
+ label="SMTP Port (Inbox)",
39
+ persist=True,
40
+ tab="smtp",
41
+ )
42
+ plugin.add_option(
43
+ "smtp_port_outbox",
44
+ type="int",
45
+ value=465,
46
+ label="SMTP Port (Outbox)",
47
+ persist=True,
48
+ tab="smtp",
49
+ )
50
+ plugin.add_option(
51
+ "smtp_user",
52
+ type="text",
53
+ value="",
54
+ label="SMTP User",
55
+ persist=True,
56
+ tab="smtp",
57
+ )
58
+ plugin.add_option(
59
+ "smtp_password",
60
+ type="text",
61
+ value="",
62
+ label="SMTP Password",
63
+ secret=True,
64
+ persist=True,
65
+ tab="smtp",
66
+ )
67
+ plugin.add_option(
68
+ "from_email",
69
+ type="text",
70
+ value="",
71
+ label="From (email)",
72
+ )
73
+ plugin.add_cmd(
74
+ "send_email",
75
+ instruction="Send email to specified address",
76
+ params=[
77
+ {
78
+ "name": "recipient",
79
+ "type": "str",
80
+ "description": "Recipient email address",
81
+ "required": True,
82
+ },
83
+ {
84
+ "name": "subject",
85
+ "type": "str",
86
+ "description": "Email subject",
87
+ "required": True,
88
+ },
89
+ {
90
+ "name": "message",
91
+ "type": "str",
92
+ "description": "Email message",
93
+ "required": True,
94
+ },
95
+ ],
96
+ enabled=True,
97
+ description="Allows sending emails",
98
+ tab="general",
99
+ )
100
+ plugin.add_cmd(
101
+ "get_emails",
102
+ instruction="Retrieve emails from the inbox. Use this to access emails from the inbox when the user "
103
+ "requests fetching emails from their server.",
104
+ params=[
105
+ {
106
+ "name": "limit",
107
+ "type": "int",
108
+ "description": "Limit of emails to receive, default: 10",
109
+ "required": True,
110
+ },
111
+ {
112
+ "name": "offset",
113
+ "type": "int",
114
+ "description": "Offset of emails to receive, default: 0",
115
+ "required": False,
116
+ },
117
+ {
118
+ "name": "order",
119
+ "type": "text",
120
+ "description": "Sort order, default: desc, options: asc, desc",
121
+ "required": False,
122
+ },
123
+ ],
124
+ enabled=True,
125
+ description="Allows receiving emails",
126
+ tab="general",
127
+ )
128
+ plugin.add_cmd(
129
+ "get_email_body",
130
+ instruction="Retrieve the complete email content from the inbox. Use this to access the message body. "
131
+ "To obtain email IDs, use the 'get_emails' function first.",
132
+ params=[
133
+ {
134
+ "name": "id",
135
+ "type": "int",
136
+ "description": "Email ID (index)",
137
+ "required": True,
138
+ },
139
+ {
140
+ "name": "format",
141
+ "type": "text",
142
+ "description": "Display format, default: text, options: text, html",
143
+ "required": False,
144
+ },
145
+ ],
146
+ enabled=True,
147
+ description="Allows receiving email body",
148
+ tab="general",
149
+ )
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2024.12.15 04:00:00 #
10
+ # ================================================== #
11
+
12
+ import datetime
13
+ import smtplib
14
+ import poplib
15
+
16
+ from email.parser import BytesParser
17
+ from typing import Any
18
+ from bs4 import BeautifulSoup
19
+
20
+ from pygpt_net.item.ctx import CtxItem
21
+
22
+
23
+ class Runner:
24
+ def __init__(self, plugin=None):
25
+ """
26
+ Cmd Runner
27
+
28
+ :param plugin: plugin
29
+ """
30
+ self.plugin = plugin
31
+ self.signals = None
32
+
33
+ def attach_signals(self, signals):
34
+ """
35
+ Attach signals
36
+
37
+ :param signals: signals
38
+ """
39
+ self.signals = signals
40
+
41
+ def parse_email(self, msg, as_text: bool = True) -> str:
42
+ """
43
+ Parse email message
44
+
45
+ :param msg: email message
46
+ :param as_text: return as text
47
+ :return: parsed email
48
+ """
49
+ body = ""
50
+ is_html = False
51
+ if msg.is_multipart():
52
+ for part in msg.walk():
53
+ content_type = part.get_content_type()
54
+ content_disposition = str(part.get('Content-Disposition'))
55
+ if content_type == 'text/plain' and 'attachment' not in content_disposition:
56
+ body = part.get_payload(decode=True).decode(part.get_content_charset('utf-8'))
57
+ elif content_type == 'text/html' and 'attachment' not in content_disposition:
58
+ body = part.get_payload(decode=True).decode(part.get_content_charset('utf-8'))
59
+ is_html = True
60
+ else:
61
+ body = msg.get_payload(decode=True).decode(msg.get_content_charset('utf-8'))
62
+ if is_html and as_text:
63
+ body = BeautifulSoup(body, 'html.parser').get_text()
64
+ return body
65
+
66
+ def smtp_get_email_body(self, ctx: CtxItem, item: dict, request: dict) -> dict:
67
+ """
68
+ Receive emails from mailbox
69
+
70
+ :param ctx: CtxItem
71
+ :param item: command item
72
+ :param request: request item
73
+ :return: response dict
74
+ """
75
+ server = self.plugin.get_option_value("smtp_host")
76
+ port = self.plugin.get_option_value("smtp_port_inbox")
77
+ id = item["params"].get("id", "")
78
+ username = self.plugin.get_option_value("smtp_user")
79
+ password = self.plugin.get_option_value("smtp_password")
80
+ msg = "Receiving email from: {}".format(server)
81
+ format = "text"
82
+ as_text = True
83
+ if "format" in item["params"]:
84
+ format = item["params"]["format"].lower()
85
+ if format == "html":
86
+ as_text = False
87
+ self.log(msg)
88
+ try:
89
+ if port in [995]:
90
+ pop3 = poplib.POP3_SSL(server, port)
91
+ else:
92
+ pop3 = poplib.POP3(server, port)
93
+
94
+ msg = pop3.getwelcome().decode('utf-8')
95
+ self.log(msg)
96
+ pop3.user(username)
97
+ pop3.pass_(password)
98
+ response, lines, octets = pop3.retr(id)
99
+ msg_content = b'\r\n'.join(lines)
100
+ msg = BytesParser().parsebytes(msg_content)
101
+ result = {
102
+ "ID": id,
103
+ "From": msg["From"],
104
+ "Subject": msg["Subject"],
105
+ "Date": msg["Date"],
106
+ "Message": self.parse_email(msg, as_text)
107
+ }
108
+ pop3.quit()
109
+ except Exception as e:
110
+ self.error(e)
111
+ result = "Error: {}".format(str(e))
112
+ return {
113
+ "request": request,
114
+ "result": str(result)
115
+ }
116
+
117
+ def smtp_receive_emails(self, ctx: CtxItem, item: dict, request: dict) -> dict:
118
+ """
119
+ Receive emails from mailbox
120
+
121
+ :param ctx: CtxItem
122
+ :param item: command item
123
+ :param request: request item
124
+ :return: response dict
125
+ """
126
+ server = self.plugin.get_option_value("smtp_host")
127
+ port = self.plugin.get_option_value("smtp_port_inbox")
128
+ limit = item["params"].get("limit", 10)
129
+ offset = item["params"].get("offset", 0)
130
+ username = self.plugin.get_option_value("smtp_user")
131
+ password = self.plugin.get_option_value("smtp_password")
132
+ msg = "Receiving emails from: {}".format(server)
133
+ order = "desc"
134
+ if "order" in item["params"]:
135
+ tmp_order = item["params"]["order"].lower()
136
+ if tmp_order in ["asc", "desc"]:
137
+ order = tmp_order
138
+ self.log(msg)
139
+
140
+ messages = []
141
+ try:
142
+ if port in [995]:
143
+ pop3 = poplib.POP3_SSL(server, port)
144
+ else:
145
+ pop3 = poplib.POP3(server, port)
146
+
147
+ msg = pop3.getwelcome().decode('utf-8')
148
+ self.log(msg)
149
+ pop3.user(username)
150
+ pop3.pass_(password)
151
+
152
+ message_count, mailbox_size = pop3.stat()
153
+ total = message_count
154
+ print(f'Num of messages: {message_count}, Inbox size: {mailbox_size} bytes')
155
+
156
+ if order == "desc":
157
+ if offset > 0:
158
+ message_count -= offset
159
+ if message_count < limit:
160
+ limit = message_count
161
+ range_max = message_count - limit
162
+ if limit == 0:
163
+ range_max = 0
164
+ for i in range(message_count, range_max, -1):
165
+ response, lines, octets = pop3.top(i, 0)
166
+ msg_content = b'\r\n'.join(lines)
167
+ msg = BytesParser().parsebytes(msg_content)
168
+ msg_body = {
169
+ "ID": i,
170
+ "From": msg["From"],
171
+ "Subject": msg["Subject"],
172
+ "Date": msg["Date"],
173
+ }
174
+ messages.append(msg_body)
175
+ else:
176
+ if limit == 0:
177
+ limit = message_count
178
+ else:
179
+ limit += offset
180
+ if limit > message_count:
181
+ limit = message_count
182
+ for i in range(offset + 1, limit + 1):
183
+ response, lines, octets = pop3.top(i, 0)
184
+ msg_content = b'\r\n'.join(lines)
185
+ msg = BytesParser().parsebytes(msg_content)
186
+ msg_body = {
187
+ "ID": i,
188
+ "From": msg["From"],
189
+ "Subject": msg["Subject"],
190
+ "Date": msg["Date"],
191
+ }
192
+ messages.append(msg_body)
193
+
194
+ pop3.quit()
195
+ result = {
196
+ "total": total,
197
+ "messages": messages,
198
+ }
199
+ except Exception as e:
200
+ self.error(e)
201
+ result = "Error: {}".format(str(e))
202
+ return {
203
+ "request": request,
204
+ "result": str(result)
205
+ }
206
+
207
+ def smtp_send_email(self, ctx: CtxItem, item: dict, request: dict) -> dict:
208
+ """
209
+ Execute system command on host
210
+
211
+ :param ctx: CtxItem
212
+ :param item: command item
213
+ :param request: request item
214
+ :return: response dict
215
+ """
216
+ msg = "Sending email to: {}".format(item["params"]['recipient'])
217
+ self.log(msg)
218
+ try:
219
+ email_to = item["params"]['recipient']
220
+ message = item["params"]['message']
221
+ subject = item["params"]['subject']
222
+ from_addr = self.plugin.get_option_value("from_email")
223
+ host = self.plugin.get_option_value("smtp_host")
224
+ port = self.plugin.get_option_value("smtp_port_outbox")
225
+ user = self.plugin.get_option_value("smtp_user")
226
+ password = self.plugin.get_option_value("smtp_password")
227
+ date = datetime.datetime.now().strftime("%d/%m/%Y %H:%M")
228
+ body = """From: {sender}\nTo: {to}\nDate: {date}\nSubject: {subject}\n\n{msg}
229
+ """.format(sender=from_addr, to=email_to, date=date, subject=subject, msg=message)
230
+ if port in [465]:
231
+ server = smtplib.SMTP_SSL(host, port)
232
+ else:
233
+ server = smtplib.SMTP(host, port)
234
+ server.ehlo()
235
+ if port in [587]:
236
+ server.starttls()
237
+ server.login(user, password)
238
+ server.sendmail(from_addr, email_to, body)
239
+ server.close()
240
+ result = "OK"
241
+ except Exception as e:
242
+ self.error(e)
243
+ result = "Error: {}".format(str(e))
244
+ return {
245
+ "request": request,
246
+ "result": str(result)
247
+ }
248
+
249
+ def error(self, err: Any):
250
+ """
251
+ Log error message
252
+
253
+ :param err: exception or error message
254
+ """
255
+ if self.signals is not None:
256
+ self.signals.error.emit(err)
257
+
258
+ def status(self, msg: str):
259
+ """
260
+ Send status message
261
+
262
+ :param msg: status message
263
+ """
264
+ if self.signals is not None:
265
+ self.signals.status.emit(msg)
266
+
267
+ def debug(self, msg: Any):
268
+ """
269
+ Log debug message
270
+
271
+ :param msg: message to log
272
+ """
273
+ if self.signals is not None:
274
+ self.signals.debug.emit(msg)
275
+
276
+ def log(self, msg, sandbox: bool = False):
277
+ """
278
+ Log message to console
279
+
280
+ :param msg: message to log
281
+ :param sandbox: is sandbox mode
282
+ """
283
+ full_msg = '[SMTP]' + ' ' + str(msg)
284
+ if self.signals is not None:
285
+ self.signals.log.emit(full_msg)