From 2e26ef5dc7561f02b39379749f9364932825ad16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Mon, 27 May 2024 22:38:21 +0200 Subject: [PATCH 01/20] Fixed User instantiation, minor changes --- Interface/TemplateEditor.py | 16 ++++++++++------ MailBuddy.pyproj | 1 + Tests/ConfigExporter_test.py | 11 +++++------ models.py | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Interface/TemplateEditor.py b/Interface/TemplateEditor.py index 83fcff3..6d3e763 100644 --- a/Interface/TemplateEditor.py +++ b/Interface/TemplateEditor.py @@ -53,12 +53,16 @@ def __on_html_key_clicked(self, event: Event): self.template_text.event_generate("<>") def __on_text_changed(self, event): - html_text = self.template_text.get("1.0", END) - mb_tag = "MailBuddyGap>" - replacement_text = '' - - html_text = html_text.replace("<" + mb_tag, replacement_text) - html_text = html_text.replace("") + def Update_preview(): + html_text = self.template_text.get("1.0", END) + mb_tag = "MailBuddyGap>" + replacement_text = '' + + html_text = html_text.replace("<" + mb_tag, replacement_text) + html_text = html_text.replace("") + + Update_preview() + self.template_preview.set_html(html_text) diff --git a/MailBuddy.pyproj b/MailBuddy.pyproj index b399d8d..ba8d0c5 100644 --- a/MailBuddy.pyproj +++ b/MailBuddy.pyproj @@ -13,6 +13,7 @@ Standard Python launcher Global|PythonCore|3.12 Pytest + True diff --git a/Tests/ConfigExporter_test.py b/Tests/ConfigExporter_test.py index b903534..fb8f069 100644 --- a/Tests/ConfigExporter_test.py +++ b/Tests/ConfigExporter_test.py @@ -1,18 +1,17 @@ +from collections.abc import Callable import pytest import json from pathlib import Path -from DataSources.dataSources import DatabaseHandler from UserInfo.cfgExporter import ConfigExporter, ExportLocation from models import Contact -from dataGenerators import genContact # from models import __all__ as modelClassNames @pytest.fixture -def getSampleJsonPath(tmp_path) -> Path: +def getSampleJsonPath(tmp_path: Path) -> Path: return tmp_path / "test.json" -def test_json_exporter_factory(getSampleJsonPath): +def test_json_exporter_factory(getSampleJsonPath: Path): p = getSampleJsonPath exporter = ConfigExporter.ToJSON(p) assert isinstance(exporter, ConfigExporter) @@ -20,7 +19,7 @@ def test_json_exporter_factory(getSampleJsonPath): assert exporter.location == p -def test_export_to_json(getSampleJsonPath): +def test_export_to_json(getSampleJsonPath: Path): filename = getSampleJsonPath exporter = ConfigExporter.ToJSON(str(filename)) exporter.Export() @@ -31,7 +30,7 @@ def test_export_to_json(getSampleJsonPath): assert isinstance(r.read(), str) -def test_export_contact_to_json(getSampleJsonPath, genContact): +def test_export_contact_to_json(getSampleJsonPath: Path, genContact: Callable[[], Contact]): c1 = genContact() c2 = genContact() assert c1 != c2 diff --git a/models.py b/models.py index 77e4384..39f8f19 100644 --- a/models.py +++ b/models.py @@ -180,7 +180,7 @@ class User(): def __init__(self, first_name: str, last_name: str, email: str, password: str) -> None: - self.contact = Contact(first_name=first_name, last_name=last_name, email=email) + self.contact = Contact(_first_name=first_name, _last_name=last_name, _email=email) self.password = password User.all_instances.append(self) IModel.queueSave(child=self) From 1729b73620d669f23a3fd5387e692f537f844dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Mon, 27 May 2024 23:37:13 +0200 Subject: [PATCH 02/20] Poprawienie umiejscowienia ComboBoxa w TemplateEditor, dodanie tabeli User --- DataSources/dataSources.py | 14 +++++-- Interface/TemplateEditor.py | 76 +++++++++++++++++++++---------------- main.py | 7 ++-- models.py | 16 +++++--- 4 files changed, 68 insertions(+), 45 deletions(-) diff --git a/DataSources/dataSources.py b/DataSources/dataSources.py index 3a5a436..6069be8 100644 --- a/DataSources/dataSources.py +++ b/DataSources/dataSources.py @@ -7,6 +7,7 @@ from models import IModel, Contact import sqlalchemy as alchem import sqlalchemy.orm as orm +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.hybrid import hybrid_property class SupportedDbEngines(Enum): @@ -118,10 +119,15 @@ def LoadSavedState(self) -> None: def Save(self, obj: IModel | GroupContacts): Session = orm.sessionmaker(bind=self.dbEngineInstance) - with Session() as session: - session.add(obj) - session.commit() - session.refresh(obj) + try: + with Session() as session: + session.add(obj) + session.commit() + session.refresh(obj) + except IntegrityError as ie: + print(ie) + except Exception as e: + print(e) self.dbEngineInstance.dispose() def DeleteEntry(self, obj: IModel | GroupContacts): diff --git a/Interface/TemplateEditor.py b/Interface/TemplateEditor.py index 6d3e763..624f52c 100644 --- a/Interface/TemplateEditor.py +++ b/Interface/TemplateEditor.py @@ -8,14 +8,19 @@ class TemplateEditor(Toplevel): + placeholder_text = " " + combo_values: list[str] = [] + def __init__(self, parent: Toplevel | Tk, master: Misc, obj: Template | None = None): super().__init__(master) self.parent = parent self.current_combo: Combobox = None self.currentTemplate = obj + self.update_combo_values() self.prepareInterface() + def prepareInterface(self): self.title("Stwórz szablon") @@ -29,6 +34,7 @@ def prepareInterface(self): self.template_text = HTMLText(self, bg="lightblue", fg="black", wrap=WORD) self.template_text.bind("", self.__on_html_key_clicked) self.template_text.bind("<>", self.__on_text_changed) + self.template_text.bind("", self.__show_placeholder_menu) # RMB self.template_preview = HTMLLabel(self, bg="lightblue", fg="black", wrap=WORD) btn_save = Button(self, text="Zapisz", bg="lightblue", fg="black", @@ -48,72 +54,78 @@ def prepareInterface(self): self.template_text.insert(INSERT, self.currentTemplate.content if self.currentTemplate.content is not None else "") self.template_text.event_generate("<>") + + def update_combo_values(self, placeholders: list[GapFillSource] = GapFillSource.all_instances): + TemplateEditor.combo_values = [key for placeholder in placeholders for key in placeholder.possible_values] + + def __on_html_key_clicked(self, event: Event): if event.keycode not in NonAlteringKeyCodes: self.template_text.event_generate("<>") + def __on_text_changed(self, event): - def Update_preview(): + def update_preview(): html_text = self.template_text.get("1.0", END) mb_tag = "MailBuddyGap>" replacement_text = '' html_text = html_text.replace("<" + mb_tag, replacement_text) html_text = html_text.replace("") + self.template_preview.set_html(html_text) - Update_preview() + update_preview() - self.template_preview.set_html(html_text) - def __save_template_clicked(self, template_name: str, template_content: str) -> None: if template_name != "" and template_content != "": self.currentTemplate = Template(_name=template_name, _content=template_content) self.parent.add_template(self.currentTemplate) self.destroy() + def hide_combobox(self): if self.current_combo: self.current_combo.destroy() - def __template_window_insert_placeholder(self, placeholders: list[GapFillSource] = GapFillSource.all_instances) -> None: - placeholder_text = " " - combo_values = [key for placeholder in placeholders for key in placeholder.possible_values] - - def on_placeholder_selection(event): - selected_placeholder = self.current_combo.get() - if selected_placeholder: - selected_text = self.template_text.tag_ranges(SEL) - if selected_text: - self.template_text.delete(selected_text[0], selected_text[1]) - self.template_text.insert(INSERT, placeholder_text.replace(" ", selected_placeholder)) - self.template_text.event_generate("<>") - - def show_placeholder_menu(event): - self.hide_combobox() - self.current_combo = Combobox(self.template_text, values=combo_values) - self.current_combo.bind("<>", on_placeholder_selection) - self.current_combo.place(x=event.x_root, y=event.y_root) - self.current_combo.focus_set() - - close_button = Button(self.current_combo, text="X", command=self.hide_combobox, bg="white") - close_button.place(relx=0.90, rely=0, anchor="ne") - - self.template_text.insert(INSERT, placeholder_text) - self.template_text.tag_configure("placeholder", background="lightgreen") - self.template_text.bind("", show_placeholder_menu) + def __template_window_insert_placeholder(self) -> None: + self.template_text.insert(INSERT, TemplateEditor.placeholder_text) + self.template_text.tag_configure("placeholder", background="lightgreen") start_index = "1.0" while True: - start_index = self.template_text.search(placeholder_text, start_index, stopindex=END) + start_index = self.template_text.search(TemplateEditor.placeholder_text, start_index, stopindex=END) if not start_index: break - end_index = self.template_text.index(f"{start_index}+{len(placeholder_text)}c") + end_index = self.template_text.index(f"{start_index}+{len(TemplateEditor.placeholder_text)}c") self.template_text.tag_add("placeholder", start_index, end_index) start_index = end_index + def __show_placeholder_menu(self, event): + self.hide_combobox() + self.current_combo = Combobox(self.template_text, values=TemplateEditor.combo_values) + self.current_combo.bind("<>", self.__on_placeholder_selection) + + self.current_combo.place(x=event.x+10, # self.winfo_pointerx(), #event.x_root + y=event.y+10) #self.winfo_pointery()) #event.y_root + self.current_combo.focus_set() + + close_button = Button(self.current_combo, text="X", command=self.hide_combobox, bg="white") + close_button.place(relx=0.90, rely=0, anchor="ne") + + + def __on_placeholder_selection(self, event): + selected_placeholder = self.current_combo.get() + if selected_placeholder: + selected_text = self.template_text.tag_ranges(SEL) + if selected_text: + self.template_text.delete(selected_text[0], selected_text[1]) + self.template_text.insert(INSERT, TemplateEditor.placeholder_text.replace(" ", selected_placeholder)) + self.template_text.event_generate("<>") + + class NonAlteringKeyCodes(Enum): # List is non-exhaustive, should be tested # via https://asawicki.info/nosense/doc/devices/keyboard/key_codes.html diff --git a/main.py b/main.py index 581ad72..5b40fbd 100644 --- a/main.py +++ b/main.py @@ -24,7 +24,7 @@ dbname = "localSQLite.sqlite3" dbURL = f"sqlite:///{dbname}" -tables = [Template, Attachment, Contact, ITrigger, Message, Group, MessageAttachment, SendAttempt, GroupContacts] +tables = [Template, Attachment, Contact, User, ITrigger, Message, Group, MessageAttachment, SendAttempt, GroupContacts] db: IDataSource = None @@ -67,8 +67,9 @@ def pushQueuedInstances(): last_name=mock_lastname, password=mock_pwd) sender = SMTPSender(mock_user) - except AttributeError as ae: - print(ae) + except Exception as e: + print(e) + ui.add_periodic_task(5000, pushQueuedInstances) ui.run() diff --git a/models.py b/models.py index 39f8f19..453adc9 100644 --- a/models.py +++ b/models.py @@ -1,12 +1,8 @@ from __future__ import annotations from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from email.mime.application import MIMEApplication -from sqlalchemy import Column, Integer, String, LargeBinary, TIMESTAMP, func +from sqlalchemy import Column, ForeignKey, Integer, String, LargeBinary, TIMESTAMP, func from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.ext.hybrid import hybrid_property -from abc import ABCMeta, abstractmethod -from pathlib import Path import re @@ -75,6 +71,7 @@ def content(self, value: str | None): self._content = value #endregion + class Attachment(IModel): all_instances = [] __tablename__ = "Attachments" @@ -175,8 +172,15 @@ def last_name(self, value: str | None): #endregion -class User(): +class User(IModel): all_instances = [] + __tablename__ = "Users" + + _id = Column("_id", Integer, primary_key=True) + _email = Column("email", String(100), ForeignKey('Contacts.email')) + + contactRel = relationship(Contact, foreign_keys=[_email]) + def __init__(self, first_name: str, last_name: str, email: str, password: str) -> None: From 1ac881fa61e42639eb35271234d628faf57f06b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Mon, 27 May 2024 23:37:13 +0200 Subject: [PATCH 03/20] Poprawienie umiejscowienia ComboBoxa w TemplateEditor, dodanie tabeli User --- DataSources/dataSources.py | 14 ++++-- Interface/TemplateEditor.py | 87 +++++++++++++++++++++---------------- main.py | 7 +-- models.py | 16 ++++--- 4 files changed, 74 insertions(+), 50 deletions(-) diff --git a/DataSources/dataSources.py b/DataSources/dataSources.py index 3a5a436..6069be8 100644 --- a/DataSources/dataSources.py +++ b/DataSources/dataSources.py @@ -7,6 +7,7 @@ from models import IModel, Contact import sqlalchemy as alchem import sqlalchemy.orm as orm +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.hybrid import hybrid_property class SupportedDbEngines(Enum): @@ -118,10 +119,15 @@ def LoadSavedState(self) -> None: def Save(self, obj: IModel | GroupContacts): Session = orm.sessionmaker(bind=self.dbEngineInstance) - with Session() as session: - session.add(obj) - session.commit() - session.refresh(obj) + try: + with Session() as session: + session.add(obj) + session.commit() + session.refresh(obj) + except IntegrityError as ie: + print(ie) + except Exception as e: + print(e) self.dbEngineInstance.dispose() def DeleteEntry(self, obj: IModel | GroupContacts): diff --git a/Interface/TemplateEditor.py b/Interface/TemplateEditor.py index 6d3e763..c2a9aa8 100644 --- a/Interface/TemplateEditor.py +++ b/Interface/TemplateEditor.py @@ -1,6 +1,6 @@ from enum import Enum from tkinter import Event, Tk, Button, Label, Entry, Toplevel, Misc -from tkinter.ttk import Combobox +from tkinter.ttk import Combobox, Frame from tkinter.constants import END, INSERT, SEL, WORD from models import Template from tkhtmlview import HTMLLabel, HTMLText @@ -8,14 +8,18 @@ class TemplateEditor(Toplevel): + placeholder_text = " " + def __init__(self, parent: Toplevel | Tk, master: Misc, obj: Template | None = None): super().__init__(master) self.parent = parent - self.current_combo: Combobox = None + self.combo_frame: Frame = None self.currentTemplate = obj + self.update_combo_values() self.prepareInterface() + def prepareInterface(self): self.title("Stwórz szablon") @@ -29,6 +33,7 @@ def prepareInterface(self): self.template_text = HTMLText(self, bg="lightblue", fg="black", wrap=WORD) self.template_text.bind("", self.__on_html_key_clicked) self.template_text.bind("<>", self.__on_text_changed) + self.template_text.bind("", self.__show_placeholder_menu) # RMB self.template_preview = HTMLLabel(self, bg="lightblue", fg="black", wrap=WORD) btn_save = Button(self, text="Zapisz", bg="lightblue", fg="black", @@ -48,72 +53,80 @@ def prepareInterface(self): self.template_text.insert(INSERT, self.currentTemplate.content if self.currentTemplate.content is not None else "") self.template_text.event_generate("<>") + + def update_combo_values(self, placeholders: list[GapFillSource] = GapFillSource.all_instances): + self.combo_values = [key for placeholder in placeholders for key in placeholder.possible_values] + + def __on_html_key_clicked(self, event: Event): if event.keycode not in NonAlteringKeyCodes: self.template_text.event_generate("<>") + def __on_text_changed(self, event): - def Update_preview(): + def update_preview(): html_text = self.template_text.get("1.0", END) mb_tag = "MailBuddyGap>" replacement_text = '' html_text = html_text.replace("<" + mb_tag, replacement_text) html_text = html_text.replace("") + self.template_preview.set_html(html_text) - Update_preview() + update_preview() - self.template_preview.set_html(html_text) - def __save_template_clicked(self, template_name: str, template_content: str) -> None: if template_name != "" and template_content != "": self.currentTemplate = Template(_name=template_name, _content=template_content) self.parent.add_template(self.currentTemplate) self.destroy() + def hide_combobox(self): - if self.current_combo: - self.current_combo.destroy() - - def __template_window_insert_placeholder(self, placeholders: list[GapFillSource] = GapFillSource.all_instances) -> None: - placeholder_text = " " - combo_values = [key for placeholder in placeholders for key in placeholder.possible_values] - - def on_placeholder_selection(event): - selected_placeholder = self.current_combo.get() - if selected_placeholder: - selected_text = self.template_text.tag_ranges(SEL) - if selected_text: - self.template_text.delete(selected_text[0], selected_text[1]) - self.template_text.insert(INSERT, placeholder_text.replace(" ", selected_placeholder)) - self.template_text.event_generate("<>") - - def show_placeholder_menu(event): - self.hide_combobox() - self.current_combo = Combobox(self.template_text, values=combo_values) - self.current_combo.bind("<>", on_placeholder_selection) - self.current_combo.place(x=event.x_root, y=event.y_root) - self.current_combo.focus_set() - - close_button = Button(self.current_combo, text="X", command=self.hide_combobox, bg="white") - close_button.place(relx=0.90, rely=0, anchor="ne") - - self.template_text.insert(INSERT, placeholder_text) - self.template_text.tag_configure("placeholder", background="lightgreen") + if self.combo_frame: + self.combo_frame.destroy() - self.template_text.bind("", show_placeholder_menu) + + def __template_window_insert_placeholder(self) -> None: + self.template_text.insert(INSERT, TemplateEditor.placeholder_text) + self.template_text.tag_configure("placeholder", background="lightgreen") start_index = "1.0" while True: - start_index = self.template_text.search(placeholder_text, start_index, stopindex=END) + start_index = self.template_text.search(TemplateEditor.placeholder_text, start_index, stopindex=END) if not start_index: break - end_index = self.template_text.index(f"{start_index}+{len(placeholder_text)}c") + end_index = self.template_text.index(f"{start_index}+{len(TemplateEditor.placeholder_text)}c") self.template_text.tag_add("placeholder", start_index, end_index) start_index = end_index + def __show_placeholder_menu(self, event): + self.hide_combobox() + self.combo_frame = Frame(self.template_text) + self.combo_frame.place(x=event.x+10, y=event.y+10) + current_combo = Combobox(self.combo_frame, values=self.combo_values) + current_combo.grid(row=0, column=0, sticky="nw") + current_combo.bind("<>", self.__on_placeholder_selection) + + current_combo.focus_set() + + close_button = Button(self.combo_frame, text="X", command=self.hide_combobox, bg="white") + close_button.grid(row=0, column=1, sticky="ne") + + + + def __on_placeholder_selection(self, event): + selected_placeholder = self.combo_frame.children['!combobox'].get() + if selected_placeholder: + selected_text = self.template_text.tag_ranges(SEL) + if selected_text: + self.template_text.delete(selected_text[0], selected_text[1]) + self.template_text.insert(INSERT, TemplateEditor.placeholder_text.replace(" ", selected_placeholder)) + self.template_text.event_generate("<>") + + class NonAlteringKeyCodes(Enum): # List is non-exhaustive, should be tested # via https://asawicki.info/nosense/doc/devices/keyboard/key_codes.html diff --git a/main.py b/main.py index 581ad72..5b40fbd 100644 --- a/main.py +++ b/main.py @@ -24,7 +24,7 @@ dbname = "localSQLite.sqlite3" dbURL = f"sqlite:///{dbname}" -tables = [Template, Attachment, Contact, ITrigger, Message, Group, MessageAttachment, SendAttempt, GroupContacts] +tables = [Template, Attachment, Contact, User, ITrigger, Message, Group, MessageAttachment, SendAttempt, GroupContacts] db: IDataSource = None @@ -67,8 +67,9 @@ def pushQueuedInstances(): last_name=mock_lastname, password=mock_pwd) sender = SMTPSender(mock_user) - except AttributeError as ae: - print(ae) + except Exception as e: + print(e) + ui.add_periodic_task(5000, pushQueuedInstances) ui.run() diff --git a/models.py b/models.py index 39f8f19..453adc9 100644 --- a/models.py +++ b/models.py @@ -1,12 +1,8 @@ from __future__ import annotations from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from email.mime.application import MIMEApplication -from sqlalchemy import Column, Integer, String, LargeBinary, TIMESTAMP, func +from sqlalchemy import Column, ForeignKey, Integer, String, LargeBinary, TIMESTAMP, func from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.ext.hybrid import hybrid_property -from abc import ABCMeta, abstractmethod -from pathlib import Path import re @@ -75,6 +71,7 @@ def content(self, value: str | None): self._content = value #endregion + class Attachment(IModel): all_instances = [] __tablename__ = "Attachments" @@ -175,8 +172,15 @@ def last_name(self, value: str | None): #endregion -class User(): +class User(IModel): all_instances = [] + __tablename__ = "Users" + + _id = Column("_id", Integer, primary_key=True) + _email = Column("email", String(100), ForeignKey('Contacts.email')) + + contactRel = relationship(Contact, foreign_keys=[_email]) + def __init__(self, first_name: str, last_name: str, email: str, password: str) -> None: From acc03cb5160d9619b26bb960ba9a7f3e620002aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Tue, 28 May 2024 01:06:10 +0200 Subject: [PATCH 04/20] Stash excel read --- Interface/ExternalSourceImportWindow.py | 96 +++++++++++++++++++++++++ Interface/TemplateEditor.py | 16 ++++- MailBuddy.pyproj | 1 + 3 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 Interface/ExternalSourceImportWindow.py diff --git a/Interface/ExternalSourceImportWindow.py b/Interface/ExternalSourceImportWindow.py new file mode 100644 index 0000000..dbad9ab --- /dev/null +++ b/Interface/ExternalSourceImportWindow.py @@ -0,0 +1,96 @@ +from tkinter import END, Misc, Tk, Toplevel +import tkinter.messagebox as msg +import tkinter.filedialog as fd +from tkinter.ttk import Button, Label, Combobox, Treeview +from openpyxl import load_workbook + +from DataSources.dataSources import GapFillSource + + +class ExternalSourceImportWindow(Toplevel): + def __init__(self, parent: Toplevel | Tk, master: Misc) -> None: + super().__init__(master) + + self.prepareInterface() + + + def prepareInterface(self): + self.title("Importuj dane") + + self.grid_rowconfigure(2, weight=1) + self.grid_columnconfigure(1, weight=1) + self.grid_columnconfigure(3, weight=1) + + self.label = Label(self, text="Select an Excel file:") + self.select_button = Button(self, text="Browse", command=self.browse_file) + self.combobox_label = Label(self, text="Worksheet names:") + self.combobox = Combobox(self) + self.combobox.bind("<>", self.update_preview) + self.treeview = Treeview(self) + + self.label.grid(row=0, column=0, padx=10, pady=10) + self.select_button.grid(row=0, column=1, padx=10, pady=10) + self.combobox_label.grid(row=1, column=0, padx=10, pady=10) + self.combobox.grid(row=1, column=1, padx=10, pady=10, sticky="ew") + self.treeview.grid(row=2, column=0, columnspan=5, padx=10, pady=10, sticky="nsew") + + + def browse_file(self): + self.file_path = fd.askopenfilename( + filetypes=[("Excel files", "*.xlsx;*.xlsm"), ("All files", "*.*")] + ) + + if self.file_path: + self.load_worksheets() + + + def update_preview(self, event=None): + selected_sheet = self.combobox.get() + if not selected_sheet or not self.file_path: + return + + try: + workbook = load_workbook(self.file_path) + sheet = workbook[selected_sheet] + preview_data = [] + + for i, column in enumerate(sheet.iter_cols(values_only=True)): + self.treeview.heading(i+1, text=column) + for row in sheet.iter_rows(min_row=1, max_row=5, values_only=True): + preview_data.append(row) + + self.treeview.configure(state='normal') + self.treeview.delete('1.0', END) + + for row in preview_data: + self.treeview.insert(END, f"{row}\n") + + self.treeview.configure(state='disabled') + except Exception as e: + msg.showerror("Error", f"Failed to read the selected worksheet: {e}") + + + def load_worksheets(self): + try: + workbook = load_workbook(self.file_path) + sheet_names = workbook.sheetnames + self.combobox['values'] = sheet_names + if sheet_names: + self.combobox.current(0) # Set the first sheet as the default selection + except Exception as e: + msg.showerror("Error", f"Failed to read the Excel file: {e}") + + + def save_data(self): + selected_sheet = self.combobox.get() + if not selected_sheet or not self.file_path: + msg.showwarning("Warning", "No worksheet selected or file not loaded.") + return + + try: + # GapFillSource() + # self.destroy() + pass + except Exception as e: + msg.showerror("Error", f"Failed to create GapFillSource: {e}") + diff --git a/Interface/TemplateEditor.py b/Interface/TemplateEditor.py index c2a9aa8..b6a2f97 100644 --- a/Interface/TemplateEditor.py +++ b/Interface/TemplateEditor.py @@ -5,6 +5,7 @@ from models import Template from tkhtmlview import HTMLLabel, HTMLText from DataSources.dataSources import GapFillSource +from .ExternalSourceImportWindow import ExternalSourceImportWindow class TemplateEditor(Toplevel): @@ -40,13 +41,17 @@ def prepareInterface(self): command=lambda: self.__save_template_clicked(name_entry.get(), self.template_text.get(1.0, END))) btn_insert_placeholder = Button(self, text="Wstaw lukę", bg="lightblue", fg="black", command=self.__template_window_insert_placeholder) + btn_add_external_source = Button(self, text="Dodaj zewnętrzne źródło", bg="lightblue", fg="black", + command=self.__add_external_source_clicked) name_label.grid(row=0, column=0, padx=5, pady=5) name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") self.template_text.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="nsew") self.template_preview.grid(row=1, column=3, columnspan=5, padx=5, pady=5, sticky="nsew") - btn_save.grid(row=2, column=2, padx=(50, 5), pady=5, sticky="e") - btn_insert_placeholder.grid(row=2, column=3, padx=(5, 50), pady=5, sticky="w") + + btn_add_external_source.grid(row=2, column=1, padx=(5, 50), pady=5, sticky="w") + btn_insert_placeholder.grid(row=2, column=2, padx=(5, 50), pady=5, sticky="w") + btn_save.grid(row=2, column=3, padx=(50, 5), pady=5, sticky="e") if self.currentTemplate: name_entry.insert(INSERT, self.currentTemplate.name if self.currentTemplate.name is not None else "") @@ -58,6 +63,10 @@ def update_combo_values(self, placeholders: list[GapFillSource] = GapFillSource. self.combo_values = [key for placeholder in placeholders for key in placeholder.possible_values] + def __add_external_source_clicked(self): + ExternalSourceImportWindow(self, self.parent.root) + + def __on_html_key_clicked(self, event: Event): if event.keycode not in NonAlteringKeyCodes: self.template_text.event_generate("<>") @@ -118,7 +127,8 @@ def __show_placeholder_menu(self, event): def __on_placeholder_selection(self, event): - selected_placeholder = self.combo_frame.children['!combobox'].get() + cb: Combobox = self.combo_frame.children['!combobox'] # type: ignore + selected_placeholder = cb.get() if selected_placeholder: selected_text = self.template_text.tag_ranges(SEL) if selected_text: diff --git a/MailBuddy.pyproj b/MailBuddy.pyproj index ba8d0c5..1d812d8 100644 --- a/MailBuddy.pyproj +++ b/MailBuddy.pyproj @@ -32,6 +32,7 @@ + From 4ac6cff679cbcd70f96dc58a7f08dfbc4be4015d Mon Sep 17 00:00:00 2001 From: dittko Date: Tue, 28 May 2024 07:50:46 +0200 Subject: [PATCH 05/20] exel wczytany --- Interface/ExternalSourceImportWindow.py | 71 ++++++++++--------------- 1 file changed, 28 insertions(+), 43 deletions(-) diff --git a/Interface/ExternalSourceImportWindow.py b/Interface/ExternalSourceImportWindow.py index dbad9ab..a16d29e 100644 --- a/Interface/ExternalSourceImportWindow.py +++ b/Interface/ExternalSourceImportWindow.py @@ -1,19 +1,16 @@ from tkinter import END, Misc, Tk, Toplevel import tkinter.messagebox as msg import tkinter.filedialog as fd -from tkinter.ttk import Button, Label, Combobox, Treeview +from tkinter.ttk import Button, Label, Combobox, Treeview, Scrollbar from openpyxl import load_workbook -from DataSources.dataSources import GapFillSource - - class ExternalSourceImportWindow(Toplevel): def __init__(self, parent: Toplevel | Tk, master: Misc) -> None: super().__init__(master) self.prepareInterface() + self.file_path = None - def prepareInterface(self): self.title("Importuj dane") @@ -26,14 +23,20 @@ def prepareInterface(self): self.combobox_label = Label(self, text="Worksheet names:") self.combobox = Combobox(self) self.combobox.bind("<>", self.update_preview) - self.treeview = Treeview(self) + + self.treeview = Treeview(self, show="headings") + self.treeview_scroll = Scrollbar(self, orient="vertical", command=self.treeview.yview) + self.treeview.configure(yscrollcommand=self.treeview_scroll.set) + + self.add_button = Button(self, text="Add", command=self.add_data) self.label.grid(row=0, column=0, padx=10, pady=10) self.select_button.grid(row=0, column=1, padx=10, pady=10) self.combobox_label.grid(row=1, column=0, padx=10, pady=10) self.combobox.grid(row=1, column=1, padx=10, pady=10, sticky="ew") - self.treeview.grid(row=2, column=0, columnspan=5, padx=10, pady=10, sticky="nsew") - + self.treeview.grid(row=2, column=0, columnspan=4, padx=10, pady=10, sticky="nsew") + self.treeview_scroll.grid(row=2, column=4, sticky="ns") + self.add_button.grid(row=3, column=0, columnspan=5, padx=10, pady=10) def browse_file(self): self.file_path = fd.askopenfilename( @@ -42,55 +45,37 @@ def browse_file(self): if self.file_path: self.load_worksheets() - - - def update_preview(self, event=None): - selected_sheet = self.combobox.get() - if not selected_sheet or not self.file_path: - return - - try: - workbook = load_workbook(self.file_path) - sheet = workbook[selected_sheet] - preview_data = [] - - for i, column in enumerate(sheet.iter_cols(values_only=True)): - self.treeview.heading(i+1, text=column) - for row in sheet.iter_rows(min_row=1, max_row=5, values_only=True): - preview_data.append(row) - - self.treeview.configure(state='normal') - self.treeview.delete('1.0', END) - - for row in preview_data: - self.treeview.insert(END, f"{row}\n") - - self.treeview.configure(state='disabled') - except Exception as e: - msg.showerror("Error", f"Failed to read the selected worksheet: {e}") - def load_worksheets(self): try: - workbook = load_workbook(self.file_path) + workbook = load_workbook(self.file_path, read_only=True) sheet_names = workbook.sheetnames self.combobox['values'] = sheet_names if sheet_names: self.combobox.current(0) # Set the first sheet as the default selection + self.update_preview() except Exception as e: msg.showerror("Error", f"Failed to read the Excel file: {e}") - - def save_data(self): + def update_preview(self, event=None): selected_sheet = self.combobox.get() if not selected_sheet or not self.file_path: - msg.showwarning("Warning", "No worksheet selected or file not loaded.") return try: - # GapFillSource() - # self.destroy() - pass + workbook = load_workbook(self.file_path, read_only=True) + sheet = workbook[selected_sheet] + + self.treeview.delete(*self.treeview.get_children()) + self.treeview["columns"] = [f"col{i}" for i in range(1, sheet.max_column + 1)] + for i, col in enumerate(self.treeview["columns"], start=1): + self.treeview.heading(col, text=f"Column {i}") + self.treeview.column(col, width=100) + + for row in sheet.iter_rows(values_only=True): + self.treeview.insert("", END, values=row) except Exception as e: - msg.showerror("Error", f"Failed to create GapFillSource: {e}") + msg.showerror("Error", f"Failed to read the selected worksheet: {e}") + def add_data(self): + msg.showinfo("Add Data", "This functionality will be implemented later.") \ No newline at end of file From 1769f63cc94c1ecb57d9d07778bc4faa87dccf2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Tue, 28 May 2024 19:14:46 +0200 Subject: [PATCH 06/20] =?UTF-8?q?Poprawienie=20import=C3=B3w?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Interface/AddContactWindow.py | 2 +- Interface/ContactList.py | 5 ++--- Interface/GroupEditor.py | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Interface/AddContactWindow.py b/Interface/AddContactWindow.py index c36d854..8648394 100644 --- a/Interface/AddContactWindow.py +++ b/Interface/AddContactWindow.py @@ -14,7 +14,7 @@ class AddContactWindow(Toplevel): - def __init__(self, parent: Toplevel | ContactList) -> None: + def __init__(self, parent: Toplevel) -> None: super().__init__(parent) self.parent = parent diff --git a/Interface/ContactList.py b/Interface/ContactList.py index 7d7d4d9..66b44fd 100644 --- a/Interface/ContactList.py +++ b/Interface/ContactList.py @@ -11,11 +11,10 @@ from models import Contact, IModel, Template, Group from tkhtmlview import HTMLLabel, HTMLText from DataSources.dataSources import GapFillSource -from GroupEditor import GroupEditor -from AddContactWindow import AddContactWindow +from .AddContactWindow import AddContactWindow class ContactList(Toplevel): - def __init__(self, parent: Toplevel | GroupEditor, group: Group | None = None) -> None: + def __init__(self, parent: Toplevel, group: Group | None = None) -> None: super().__init__(parent) self.group = group self.parent = parent diff --git a/Interface/GroupEditor.py b/Interface/GroupEditor.py index 221ca9f..3738dd8 100644 --- a/Interface/GroupEditor.py +++ b/Interface/GroupEditor.py @@ -2,6 +2,7 @@ from tkinter.constants import END, INSERT, WORD from group_controller import GroupController from models import Contact, Group +from .ContactList import ContactList class GroupEditor(Toplevel): From 345ad26ce7af8a36594e51c46286168a86fa6b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Tue, 28 May 2024 19:21:42 +0200 Subject: [PATCH 07/20] Fix group add contact --- Interface/ContactList.py | 8 +++----- Interface/GroupEditor.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Interface/ContactList.py b/Interface/ContactList.py index 66b44fd..0ecc170 100644 --- a/Interface/ContactList.py +++ b/Interface/ContactList.py @@ -59,7 +59,7 @@ def update(self): self.populateWindow() def populateWindow(self): - shouldAddButton = self.parent != None and isinstance(self.parent, GroupEditor) + shouldAddButton = self.parent != None for idx, c in enumerate(Contact.all_instances): self.create_contact_widget(c, idx, addBtn=shouldAddButton) @@ -90,15 +90,13 @@ def add_contact_to_group(self, c: Contact): try: GroupController.add_contact(self.group, c) - if isinstance(self.parent, GroupEditor): - self.parent.update() + self.parent.update() except IntegrityError: pass def remove_contact_from_group(self, c: Contact): GroupController.delete_connection(self.group, c) - if isinstance(self.parent, GroupEditor): - self.parent.update() + self.parent.update() def search_contact(self): search_criteria = self.search_entry.get().strip() diff --git a/Interface/GroupEditor.py b/Interface/GroupEditor.py index 3738dd8..1e5f504 100644 --- a/Interface/GroupEditor.py +++ b/Interface/GroupEditor.py @@ -9,7 +9,7 @@ class GroupEditor(Toplevel): def __init__(self, parent: Toplevel | Tk, edited: Group | None = None): super().__init__(parent.root) self.parent = parent - self.currentGroup = edited + self.currentGroup = Group() if edited == None else edited def prepareInterface(self): name_label = Label(self, text="Nazwa grupy:", bg="lightblue") From e1eabb00eb8da674dd97fbaf8cd3dace6a2be321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Tue, 28 May 2024 22:31:33 +0200 Subject: [PATCH 08/20] Aktualizacja danych w bazie --- DataSources/dataSources.py | 19 ++++++++++++++++--- Interface/AppUI.py | 38 ++++++++++++++++++++++++-------------- Interface/ContactList.py | 11 +++++------ Interface/GroupEditor.py | 12 ++---------- group_controller.py | 4 +++- main.py | 38 +++++++++++++++----------------------- models.py | 34 ++++++++++++++++++++++++++++------ 7 files changed, 93 insertions(+), 63 deletions(-) diff --git a/DataSources/dataSources.py b/DataSources/dataSources.py index 6069be8..4eca54f 100644 --- a/DataSources/dataSources.py +++ b/DataSources/dataSources.py @@ -117,13 +117,26 @@ def LoadSavedState(self) -> None: continue IModel.run_loading = False + def Update(self, obj: IModel): + Session = orm.sessionmaker(bind=self.dbEngineInstance) + try: + with Session() as session: + session.merge(obj) + session.commit() + except IntegrityError as ie: + print(ie) + except Exception as e: + print(e) + finally: + self.dbEngineInstance.dispose() + def Save(self, obj: IModel | GroupContacts): Session = orm.sessionmaker(bind=self.dbEngineInstance) try: with Session() as session: - session.add(obj) - session.commit() - session.refresh(obj) + session.add(obj) + session.commit() + session.refresh(obj) except IntegrityError as ie: print(ie) except Exception as e: diff --git a/Interface/AppUI.py b/Interface/AppUI.py index 5812df0..e25d794 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -35,6 +35,18 @@ def prepareInterface(self) -> None: self.__create_mailing_group_pane() self.__create_template_pane() self.__create_mail_input_pane() + self.populateInterface() + + + def populateInterface(self) -> None: + modelType_func_mapper = { + Template: self.add_template, + Group: self.add_group + } + + for (modelType, ui_func) in modelType_func_mapper.items(): + ui_func(modelType.all_instances) + def add_periodic_task(self, period: int, func: Callable): # TODO można poprawić żeby się odpalało tylko przy dodaniu obiektu, @@ -69,6 +81,14 @@ def add_group(self, g: Group | Iterable[Group]): [self.grupy.append(i) for i in g if i not in self.grupy] self.__update_listbox(self.grupy_listbox, self.grupy) + def clearData(self): + self.grupy = [] + self.szablony = [] + + def update(self): + self.clearData() + self.populateInterface() + def __add_group_clicked(self): self.show_group_window() @@ -80,12 +100,6 @@ def __send_clicked(event) -> None: print("send mail") pass - def __importuj_clicked(self): - pass - - def __eksportuj_clicked(self): - pass - def __template_selection_changed(self, _event): selected = self.template_listbox.curselection() if len(selected) > 0: @@ -134,11 +148,6 @@ def __add_template_clicked(self): def __create_menu(self): menubar = Menu(self.root) - file_menu = Menu(menubar, tearoff=0) - file_menu.add_command(label="Import", command=self.__importuj_clicked) - file_menu.add_command(label="Export", command=self.__eksportuj_clicked) - menubar.add_cascade(label="File", menu=file_menu) - edit_menu = Menu(menubar, tearoff=0) add_menu = Menu(edit_menu, tearoff=0) add_menu.add_command( @@ -147,7 +156,7 @@ def __create_menu(self): add_menu.add_command(label="Group", command=self.__add_group_clicked) edit_menu.add_cascade(label="Add...", menu=add_menu) menubar.add_cascade(label="Edit", menu=edit_menu) - menubar.add_command(label="Open Settings", command=self.logout) + menubar.add_command(label="Open Settings", command=self.__openSettings_clicked) menubar.add_command(label="Send", command=lambda: self.__send_clicked()) @@ -171,6 +180,7 @@ def __create_mailing_group_pane(self): grupy_label.pack() self.grupy_listbox.pack(fill=BOTH, expand=True) + def __create_template_pane(self): templates_frame = Frame( self.root, bg="lightblue", width=200, height=100, relief=RIDGE, borderwidth=2) @@ -188,6 +198,7 @@ def __create_template_pane(self): szablony_label.pack() self.template_listbox.pack(fill=BOTH, expand=True) + def __create_mail_input_pane(self): entry_frame = Frame(self.root, bg="lightblue", relief=RIDGE, borderwidth=2) @@ -212,8 +223,7 @@ def show_template_window(self, obj: Template | None = None): self.template_window = TemplateEditor(self, self.root, obj) self.template_window.prepareInterface() - def logout(self): - + def __openSettings_clicked(self): root = Tk() # Otwórz ponownie okno logowania settings = Settings(root) settings.prepareInterface() diff --git a/Interface/ContactList.py b/Interface/ContactList.py index 0ecc170..c7f1859 100644 --- a/Interface/ContactList.py +++ b/Interface/ContactList.py @@ -60,15 +60,14 @@ def update(self): def populateWindow(self): shouldAddButton = self.parent != None - for idx, c in enumerate(Contact.all_instances): - self.create_contact_widget(c, idx, addBtn=shouldAddButton) - if self.group: group_contacts = GroupController.get_contacts(self.group) group_emails = {contact.email for contact in group_contacts} - for idx, c in enumerate(Contact.all_instances): - added_to_group = c.email in group_emails - self.create_contact_widget(c, idx, added_to_group, addBtn=shouldAddButton) + + for idx, c in enumerate(Contact.all_instances): + shouldToggle = c.email in group_emails + self.create_contact_widget(c, idx, added_to_group=shouldToggle, addBtn=shouldAddButton) + def create_contact_widget(self, c: Contact, idx: int, added_to_group: bool = False, addBtn: bool = True): def toggle_checkbox(): diff --git a/Interface/GroupEditor.py b/Interface/GroupEditor.py index 1e5f504..7bfd152 100644 --- a/Interface/GroupEditor.py +++ b/Interface/GroupEditor.py @@ -9,7 +9,7 @@ class GroupEditor(Toplevel): def __init__(self, parent: Toplevel | Tk, edited: Group | None = None): super().__init__(parent.root) self.parent = parent - self.currentGroup = Group() if edited == None else edited + self.currentGroup = edited if edited != None else Group(_name="Nowa grupa" + str(len(Group.all_instances))) def prepareInterface(self): name_label = Label(self, text="Nazwa grupy:", bg="lightblue") @@ -56,13 +56,5 @@ def __save_group_clicked(self) -> None: self.currentGroup = Group(_name = self.name_entry.get()) else: self.currentGroup.name = self.name_entry.get() - txt = self.email_text.get(1.0, END).strip() - email_addresses = [address for address in txt.replace("\n", "").split(",") if address.strip()] - # TODO: Przy zmianie kontrolek w grupie będzie trzeba zmienić wywoływanie konstruktora - te kontakty powinny być zapisane wcześniej, bez możliwości dodawania ich od tak z palca - for mail in email_addresses: - try: - self.currentGroup._add_contact(Contact(_email=mail)) - except AttributeError as e: - raise e - self.parent.add_group(self.currentGroup) + self.parent.update() self.destroy() diff --git a/group_controller.py b/group_controller.py index 9749a12..12347ff 100644 --- a/group_controller.py +++ b/group_controller.py @@ -27,5 +27,7 @@ def get_contacts(cls, g: Group) -> list[Contact]: # TODO: Wydajność? Wywołania tego na potencjalnie ogromnej tabeli to spory koszt, na pewno można to jakoś kiedyś ładnie zoptymalizować result = [] for entry in mapping: - result.append(*cls.dbh.GetData(Contact, email=entry.contact_id)) + data: list = cls.dbh.GetData(Contact, email=entry.contact_id) + if len(data) > 0: + result.append(*data) return result diff --git a/main.py b/main.py index 5b40fbd..91d4d58 100644 --- a/main.py +++ b/main.py @@ -27,45 +27,37 @@ tables = [Template, Attachment, Contact, User, ITrigger, Message, Group, MessageAttachment, SendAttempt, GroupContacts] db: IDataSource = None - -def populateInterface(app: AppUI) -> None: - modelType_func_mapper = { - Template: app.add_template, - Group: app.add_group - } - - for (modelType, ui_func) in modelType_func_mapper.items(): - ui_func(modelType.all_instances) def pushQueuedInstances(): - if len(IModel.saveQueued) > 0: - for o in IModel.saveQueued: - # match type(o): - # case type(Group): - # GroupContacts.FromGroup(o) - # case _: + if len(IModel.addQueued) > 0: + for o in IModel.addQueued: db.Save(o) - IModel.saveQueued.remove(o) + IModel.addQueued.remove(o) + if len(IModel.updateQueued) > 0: + for o in IModel.updateQueued: + db.Update(o) + IModel.updateQueued.remove(o) if __name__ == "__main__": db = DatabaseHandler(dbURL, tables) GroupController.setDbHandler(db) - ui = AppUI() - ui.prepareInterface() if db.checkIntegrity(): print("Database intact, proceeding") db.LoadSavedState() - populateInterface(ui) + + ui = AppUI() + ui.prepareInterface() + _contact_fields = GapFillSource() if (mocking_enabled): try: - mock_user = User(email=mock_login, - first_name=mock_name, - last_name=mock_lastname, - password=mock_pwd) + mock_user = User(_email=mock_login, + _first_name=mock_name, + _last_name=mock_lastname, + _password=mock_pwd) sender = SMTPSender(mock_user) except Exception as e: print(e) diff --git a/models.py b/models.py index 453adc9..6488a6e 100644 --- a/models.py +++ b/models.py @@ -12,12 +12,18 @@ class IModel(declarative_base()): __abstract__ = True run_loading = True - saveQueued: list[IModel] = [] + addQueued: list[IModel] = [] + updateQueued: list[IModel] = [] @staticmethod def queueSave(child): if not IModel.run_loading: - IModel.saveQueued.append(child) + IModel.addQueued.append(child) + + @staticmethod + def queueToUpdate(child): + if not IModel.run_loading: + IModel.updateQueued.append(child) class Template(IModel): @@ -182,12 +188,18 @@ class User(IModel): contactRel = relationship(Contact, foreign_keys=[_email]) - def __init__(self, first_name: str, last_name: str, - email: str, password: str) -> None: - self.contact = Contact(_first_name=first_name, _last_name=last_name, _email=email) - self.password = password + def __init__(self, **kwargs) -> None: + self._email = kwargs.pop("_email") + self.password = kwargs.pop("_password", None) + self.contact = self.getExistingContact(kwargs.pop("_first_name", None), kwargs.pop("_last_name", None)) User.all_instances.append(self) IModel.queueSave(child=self) + + def getExistingContact(self, first_name, last_name) -> Contact: + for c in Contact.all_instances: + if c.email == self._email: + return c + return Contact(_first_name=first_name, _last_name=last_name, _email=self._email) class Message(IModel, MIMEMultipart): @@ -226,6 +238,15 @@ def __init__(self, **kwargs): self.contacts: list[Contact] = kwargs.pop("_contacts", []) Group.all_instances.append(self) IModel.queueSave(self) + + @hybrid_property + def name(self): + return self._name + + @name.setter + def name(self, value: str | None): + self._name = value + IModel.queueToUpdate(self) def __str__(self): return f"{self.id}: {self.name}" @@ -262,4 +283,5 @@ def id(self, newValue: int): @name.setter def name(self, value: str | None): self._name = value + IModel.queueToUpdate(self) #endregion From eb057c0bc9277125d3514f8af5a2c21b1c932482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Tue, 28 May 2024 22:59:57 +0200 Subject: [PATCH 09/20] =?UTF-8?q?Fix=20wy=C5=9Bwietlanie=20podgl=C4=85du?= =?UTF-8?q?=20maili=20w=20AppUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Interface/AppUI.py | 4 +++- models.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Interface/AppUI.py b/Interface/AppUI.py index e25d794..547fafa 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -115,7 +115,9 @@ def __group_selection_changed(self, _event): selected: int = self.grupy_listbox.curselection() if len(selected) > 0: g: Group = self.grupy[selected[0]] - mails = [", ".join(x.email) for x in g.contacts] + mails = "" + for c in g.contacts: + mails += c.email + ", " self.entry_adres.delete(0, END) self.entry_adres.insert(INSERT, mails) diff --git a/models.py b/models.py index 6488a6e..efe4ac3 100644 --- a/models.py +++ b/models.py @@ -182,8 +182,9 @@ class User(IModel): all_instances = [] __tablename__ = "Users" - _id = Column("_id", Integer, primary_key=True) - _email = Column("email", String(100), ForeignKey('Contacts.email')) + _id = Column("_id", Integer, primary_key=True, autoincrement=True) + _email = Column("email", String(100), ForeignKey('Contacts.email'), unique=True) + contactRel = relationship(Contact, foreign_keys=[_email]) From 735ac40cd7182b329f4d8fe56b44e7b282fd6694 Mon Sep 17 00:00:00 2001 From: Maciej Kuczynski Date: Tue, 28 May 2024 23:06:46 +0200 Subject: [PATCH 10/20] Obtaining host server data by email address --- Interface/Settings.py | 9 ++- MessagingService/accountInfo.py | 138 ++++++++++++++++++++++++++++++++ requirements.txt | 4 +- 3 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 MessagingService/accountInfo.py diff --git a/Interface/Settings.py b/Interface/Settings.py index 127d827..5ae04fe 100644 --- a/Interface/Settings.py +++ b/Interface/Settings.py @@ -11,6 +11,7 @@ from models import Contact, IModel, Template, Group from tkhtmlview import HTMLLabel, HTMLText from DataSources.dataSources import GapFillSource +from MessagingService.accountInfo import discover_email_settings class Settings: @@ -22,7 +23,7 @@ def __init__(self, root): def prepareInterface(self): # TODO: tutaj powinniśmy ładować wartości z User - example_emails = ["example1@example.com", "example2@example.com", "example3@example.com"] + example_emails = ["kuczynskimaciej1@poczta.onet.pl", "example1@example.com", "example2@example.com", "example3@example.com"] label = Label( self.root, @@ -31,6 +32,8 @@ def prepareInterface(self): font=("Helvetica", 24)) self.email_combobox = Combobox(self.root, values=example_emails) + + self.password_entry = Entry(self.root, show="*") connect_button = Button( self.root, @@ -55,13 +58,17 @@ def prepareInterface(self): label.pack(pady=20) self.email_combobox.pack(pady=5) + self.password_entry.pack(pady=5) connect_button.pack(pady=5) change_email_button.pack(pady=5) close_button.pack(pady=5) def connect(self): email = self.email_combobox.get() + password = self.password_entry.get() + # TODO: połączenie z pocztą + email_settings = discover_email_settings(email,password) messagebox.showinfo("Połączenie", f"Połączono z {email}") def change_email(self): diff --git a/MessagingService/accountInfo.py b/MessagingService/accountInfo.py new file mode 100644 index 0000000..c27ade9 --- /dev/null +++ b/MessagingService/accountInfo.py @@ -0,0 +1,138 @@ +from dns.resolver import resolve +import requests +import smtplib +import imaplib +import xml.etree.ElementTree as ET + + +default_settings = { + 'gmail.com': { + 'imap': {'host': 'imap.gmail.com', 'port': 993, 'ssl': True}, + 'smtp': {'host': 'smtp.gmail.com', 'port': 587, 'tls': True} + } +} + + +def get_domain(email): + return email.split('@')[1] + + +def get_mx_records(domain): + try: + answers = resolve(domain, 'MX') + mx_records = [answer.exchange.to_text() for answer in answers] + return mx_records + except Exception as e: + print(f"DNS lookup failed: {e}") + return [] + + +def get_autodiscover_settings(domain): + try: + url = f'https://autoconfig.{domain}/mail/config-v1.1.xml' + response = requests.get(url) + if response.status_code == 200: + return response.text # Process XML to extract settings + else: + url = f'https://{domain}/.well-known/autoconfig/mail/config-v1.1.xml' + response = requests.get(url) + if response.status_code == 200: + return response.text # Process XML to extract settings + except Exception as e: + print(f"Autodiscover failed: {e}") + return None + + + +def parse_email_settings(xml_data): + tree = ET.ElementTree(ET.fromstring(xml_data)) + root = tree.getroot() + email_provider = root.find('emailProvider') + + settings = { + 'imap': {}, + 'pop3': {}, + 'smtp': [] + } + + for server in email_provider.findall('incomingServer'): + server_type = server.get('type') + settings[server_type] = { + 'hostname': server.find('hostname').text, + 'port': int(server.find('port').text), + 'socket_type': server.find('socketType').text + } + + for server in email_provider.findall('outgoingServer'): + smtp_settings = { + 'hostname': server.find('hostname').text, + 'port': int(server.find('port').text), + 'socket_type': server.find('socketType').text + } + settings['smtp'].append(smtp_settings) + + return settings + + + +def test_imap_connection(imap_settings, email, password): + try: + if imap_settings['socket_type'] == 'SSL': + connection = imaplib.IMAP4_SSL(imap_settings['hostname'], imap_settings['port']) + else: + connection = imaplib.IMAP4(imap_settings['hostname'], imap_settings['port']) + + connection.login(email, password) + connection.logout() + return True + except Exception as e: + print(f"IMAP connection failed: {e}") + return False + + +def test_smtp_connection(smtp_settings, email, password): + for setting in smtp_settings: + try: + if setting['socket_type'] == 'SSL': + connection = smtplib.SMTP_SSL(setting['hostname'], setting['port']) + else: + connection = smtplib.SMTP(setting['hostname'], setting['port']) + if setting['socket_type'] == 'STARTTLS': + connection.starttls() + + connection.login(email, password) + connection.quit() + return True + except Exception as e: + print(f"SMTP connection to {setting['hostname']} on port {setting['port']} failed: {e}") + return False + + +def discover_email_settings(email, password): + domain = get_domain(email) + + mx_records = get_mx_records(domain) + print(mx_records) + if mx_records: + pass + + # Try autodiscovery + settings_xml = get_autodiscover_settings(domain) + if settings_xml: + settings_xml = parse_email_settings(settings_xml) + pass + + # Use default settings + if domain in default_settings: + settings = default_settings[domain] + else: + print("No settings found for this domain.") + + # Test IMAP and SMTP connections + if test_imap_connection(settings_xml['imap'], email, password) and test_smtp_connection(settings_xml['smtp'], email, password): + print("Check ok") + print(settings_xml) + return settings_xml + else: + print("Failed to connect with discovered settings.") + return None diff --git a/requirements.txt b/requirements.txt index 9b15506..87b4a8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,6 @@ openpyxl sqlalchemy pytest faker -tkhtmlview \ No newline at end of file +tkhtmlview +dnspython +openpyxl \ No newline at end of file From c0f0a5746d1033708cfaf8353531777ff4c77eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Wed, 29 May 2024 00:44:03 +0200 Subject: [PATCH 11/20] =?UTF-8?q?Dodanie=20importu=20plik=C3=B3w=20XLSX=20?= =?UTF-8?q?do=20templatki,=20zapis=20ich=20=C5=9Bcie=C5=BCek=20i=20odczyt?= =?UTF-8?q?=20w=20Gapach?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DataSources/dataSources.py | 8 +- Interface/ExternalSourceImportWindow.py | 27 ++++-- Interface/TemplateEditor.py | 7 +- main.py | 4 +- models.py | 106 ++++++++++++++++++++++-- 5 files changed, 130 insertions(+), 22 deletions(-) diff --git a/DataSources/dataSources.py b/DataSources/dataSources.py index 4eca54f..dfd655d 100644 --- a/DataSources/dataSources.py +++ b/DataSources/dataSources.py @@ -4,7 +4,7 @@ from enum import Enum from pandas import read_csv, read_excel, DataFrame from additionalTableSetup import GroupContacts -from models import IModel, Contact +from models import DataImport, IModel, Contact import sqlalchemy as alchem import sqlalchemy.orm as orm from sqlalchemy.exc import IntegrityError @@ -143,7 +143,7 @@ def Save(self, obj: IModel | GroupContacts): print(e) self.dbEngineInstance.dispose() - def DeleteEntry(self, obj: IModel | GroupContacts): + def DeleteEntry(self, obj: IModel | GroupContacts): Session = orm.sessionmaker(bind=self.dbEngineInstance) with Session() as session: session.delete(obj) @@ -187,6 +187,8 @@ class GapFillSource(): def __init__(self, source: IDataSource | IModel = Contact) -> None: if isinstance(source, IDataSource): self.iData: IDataSource = source + elif isinstance(source, DataImport): + self.model_source: IModel = source elif issubclass(source, IModel): self.model_source: IModel = source else: @@ -211,5 +213,7 @@ def get_possible_values(self): elif hasattr(self, "model_source"): if self.model_source == Contact: self.possible_values = { name: attr for name, attr in Contact.__dict__.items() if isinstance(attr, hybrid_property) and attr != "all_instances" } + elif isinstance(self.model_source, DataImport): + self.possible_values = self.model_source.getColumnPreview() else: raise AttributeError(f"{type(self.model_source)} isn't supported") diff --git a/Interface/ExternalSourceImportWindow.py b/Interface/ExternalSourceImportWindow.py index a16d29e..8f573e2 100644 --- a/Interface/ExternalSourceImportWindow.py +++ b/Interface/ExternalSourceImportWindow.py @@ -1,13 +1,16 @@ +from os.path import basename from tkinter import END, Misc, Tk, Toplevel import tkinter.messagebox as msg import tkinter.filedialog as fd from tkinter.ttk import Button, Label, Combobox, Treeview, Scrollbar from openpyxl import load_workbook +from models import DataImport, Template class ExternalSourceImportWindow(Toplevel): - def __init__(self, parent: Toplevel | Tk, master: Misc) -> None: + def __init__(self, parent: Toplevel | Tk, master: Misc, template: Template) -> None: super().__init__(master) - + self.parent = parent + self.template = template self.prepareInterface() self.file_path = None @@ -52,7 +55,7 @@ def load_worksheets(self): sheet_names = workbook.sheetnames self.combobox['values'] = sheet_names if sheet_names: - self.combobox.current(0) # Set the first sheet as the default selection + self.combobox.current(0) self.update_preview() except Exception as e: msg.showerror("Error", f"Failed to read the Excel file: {e}") @@ -67,15 +70,23 @@ def update_preview(self, event=None): sheet = workbook[selected_sheet] self.treeview.delete(*self.treeview.get_children()) - self.treeview["columns"] = [f"col{i}" for i in range(1, sheet.max_column + 1)] - for i, col in enumerate(self.treeview["columns"], start=1): - self.treeview.heading(col, text=f"Column {i}") + first_row = next(sheet.iter_rows(values_only=True)) + if "Email" not in first_row: + # TODO: Można zrobić jakiś label zamiast treeview i errora + raise ValueError("Arkusz musi mieć kolumnę 'Email', aby dało się go połączyć z danymi") + + self.treeview["columns"] = first_row + for col in first_row: + self.treeview.heading(col, text=col) self.treeview.column(col, width=100) - for row in sheet.iter_rows(values_only=True): + for row in sheet.iter_rows(min_row=2, values_only=True): self.treeview.insert("", END, values=row) except Exception as e: msg.showerror("Error", f"Failed to read the selected worksheet: {e}") def add_data(self): - msg.showinfo("Add Data", "This functionality will be implemented later.") \ No newline at end of file + di = DataImport(_name=basename(self.file_path), _localPath=self.file_path) + self.template.dataimport = di + self.parent.update() + self.destroy() diff --git a/Interface/TemplateEditor.py b/Interface/TemplateEditor.py index b6a2f97..7294dfb 100644 --- a/Interface/TemplateEditor.py +++ b/Interface/TemplateEditor.py @@ -64,7 +64,11 @@ def update_combo_values(self, placeholders: list[GapFillSource] = GapFillSource. def __add_external_source_clicked(self): - ExternalSourceImportWindow(self, self.parent.root) + ExternalSourceImportWindow(self, self.parent.root, self.currentTemplate) + + def update(self): + GapFillSource(self.currentTemplate.dataimport) + self.update_combo_values() def __on_html_key_clicked(self, event: Event): @@ -76,6 +80,7 @@ def __on_text_changed(self, event): def update_preview(): html_text = self.template_text.get("1.0", END) mb_tag = "MailBuddyGap>" + #TODO add 1 row preview from currentTemplate.DataImport replacement_text = '' html_text = html_text.replace("<" + mb_tag, replacement_text) diff --git a/main.py b/main.py index 91d4d58..13cdaf1 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,7 @@ from UserInfo.LoginService import * # from sys import platform from group_controller import GroupController -from models import IModel, Template, Attachment, Contact, Message, Group, User +from models import DataImport, IModel, Template, Attachment, Contact, Message, Group, User from Triggers.triggers import ITrigger from Interface.AppUI import AppUI from DataSources.dataSources import DatabaseHandler, GapFillSource, IDataSource @@ -24,7 +24,7 @@ dbname = "localSQLite.sqlite3" dbURL = f"sqlite:///{dbname}" -tables = [Template, Attachment, Contact, User, ITrigger, Message, Group, MessageAttachment, SendAttempt, GroupContacts] +tables = [Template, DataImport, Attachment, Contact, User, ITrigger, Message, Group, MessageAttachment, SendAttempt, GroupContacts] db: IDataSource = None diff --git a/models.py b/models.py index efe4ac3..68f1111 100644 --- a/models.py +++ b/models.py @@ -1,5 +1,6 @@ from __future__ import annotations from email.mime.multipart import MIMEMultipart +from openpyxl import load_workbook from sqlalchemy import Column, ForeignKey, Integer, String, LargeBinary, TIMESTAMP, func from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.ext.hybrid import hybrid_property @@ -26,18 +27,94 @@ def queueToUpdate(child): IModel.updateQueued.append(child) +class DataImport(IModel): + all_instances: list[DataImport] = [] + __tablename__ = "DataImport" + + _id = Column("id", Integer, primary_key=True, autoincrement=True) + _name = Column("name", String(100)) + _localPath = Column("localPath", String(255), nullable=True) + _content = Column("content", LargeBinary, nullable=True) + + def __init__(self, **kwargs) -> None: + self.id = kwargs.pop('_id', None) + self.name = kwargs.pop('_name', None) + self.localPath = kwargs.pop('_localPath', None) + self.content = kwargs.pop('_content', None) + DataImport.all_instances.append(self) + IModel.queueSave(child=self) + + def getColumnPreview(self) -> dict | None: + workbook = load_workbook(self.localPath, read_only=True) + result = dict() + for sheet in workbook: + first_row = next(sheet.iter_rows(values_only=True)) + if "Email" not in first_row: + continue + + columns = first_row + dataPreviewRow = next(sheet.iter_rows(values_only=True)) + for idx, c in enumerate(columns): + result[c] = dataPreviewRow[idx] + return result if len(result) > 0 else None + + +# region Properties + @hybrid_property + def id(self): + return self._id + + @hybrid_property + def name(self): + return self._name + + @hybrid_property + def content(self): + return self._content + + @hybrid_property + def localPath(self): + return self._localPath + + @id.setter + def id(self, newValue: int): + self._id = newValue + + @name.setter + def name(self, value: str | None): + self._name = value + IModel.queueToUpdate(self) + + @content.setter + def content(self, value: object | None): + self._content = value + IModel.queueToUpdate(self) + + @localPath.setter + def localPath(self, value: str | None): + self._localPath = value + IModel.queueToUpdate(self) +#endregion + + class Template(IModel): all_instances: list[Template] = [] __tablename__ = "Templates" - _id = Column("id", Integer, primary_key=True) + _id = Column("id", Integer, primary_key=True, autoincrement=True) _name = Column("name", String(100), nullable=True) _content = Column("content", String, nullable=True) - + _dataimport_id = Column("dataimport_id", Integer, #ForeignKey("DataImport.id", ondelete='SET NULL'), + nullable=True) + + # dataImportRel = relationship(DataImport, foreign_keys=[DataImport._id]) + def __init__(self, **kwargs) -> None: - self.id = kwargs.pop('_id', None) - self.name = kwargs.pop('_name', None) - self.content = kwargs.pop('_content', None) + self.id: int = kwargs.pop('_id', None) + self.name: str = kwargs.pop('_name', None) + self.content: object = kwargs.pop('_content', None) + self._dataimport: DataImport = None + self.dataimport: DataImport = kwargs.pop("_dataimport_id", None) Template.all_instances.append(self) IModel.queueSave(child=self) @@ -60,21 +137,32 @@ def name(self): @hybrid_property def content(self): return self._content + + @hybrid_property + def dataimport(self) -> DataImport: + return self._dataimport @id.setter def id(self, newValue: int): - if newValue: - self._id = newValue - else: - self._id = max((i.id for i in Template.all_instances), default=0) + 1 + # TODO: if initial setup / loading from db + self._id = newValue @name.setter def name(self, value: str | None): self._name = value + IModel.queueToUpdate(self) @content.setter def content(self, value: str | None): self._content = value + IModel.queueToUpdate(self) + + @dataimport.setter + def dataimport(self, value: DataImport | None): + self._dataimport = value + if value != None: + self._dataimport_id = value.id + IModel.queueToUpdate(self) #endregion From dd9224d36c8963d9cffcb7c0f5d513a29eec3154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Wed, 29 May 2024 01:52:31 +0200 Subject: [PATCH 12/20] =?UTF-8?q?Wy=C5=9Bwietlanie=20podgl=C4=85du=20w=20T?= =?UTF-8?q?emplateEditor=20z=20podstawionym=20pogl=C4=85dowym=20tekstem=20?= =?UTF-8?q?z=20excela?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DataSources/dataSources.py | 11 ++++++++++- Interface/TemplateEditor.py | 20 +++++++++++++++----- main.py | 7 +++++++ models.py | 27 ++++++++++++++++----------- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/DataSources/dataSources.py b/DataSources/dataSources.py index dfd655d..b969e25 100644 --- a/DataSources/dataSources.py +++ b/DataSources/dataSources.py @@ -187,7 +187,7 @@ class GapFillSource(): def __init__(self, source: IDataSource | IModel = Contact) -> None: if isinstance(source, IDataSource): self.iData: IDataSource = source - elif isinstance(source, DataImport): + elif isinstance(source, DataImport) or isinstance(source, list): self.model_source: IModel = source elif issubclass(source, IModel): self.model_source: IModel = source @@ -217,3 +217,12 @@ def get_possible_values(self): self.possible_values = self.model_source.getColumnPreview() else: raise AttributeError(f"{type(self.model_source)} isn't supported") + + @staticmethod + def getPreviewText(searched: str) -> str | None: + for g in GapFillSource.all_instances: + candidate = g.possible_values.get(searched, None) + if candidate == None: + continue + return candidate + return None diff --git a/Interface/TemplateEditor.py b/Interface/TemplateEditor.py index 7294dfb..0412502 100644 --- a/Interface/TemplateEditor.py +++ b/Interface/TemplateEditor.py @@ -1,3 +1,4 @@ +import re from enum import Enum from tkinter import Event, Tk, Button, Label, Entry, Toplevel, Misc from tkinter.ttk import Combobox, Frame @@ -16,6 +17,8 @@ def __init__(self, parent: Toplevel | Tk, master: Misc, obj: Template | None = N self.parent = parent self.combo_frame: Frame = None self.currentTemplate = obj + if self.currentTemplate and self.currentTemplate.dataimport: + GapFillSource(self.currentTemplate.dataimport) self.update_combo_values() self.prepareInterface() @@ -79,12 +82,19 @@ def __on_html_key_clicked(self, event: Event): def __on_text_changed(self, event): def update_preview(): html_text = self.template_text.get("1.0", END) - mb_tag = "MailBuddyGap>" - #TODO add 1 row preview from currentTemplate.DataImport - replacement_text = '' - html_text = html_text.replace("<" + mb_tag, replacement_text) - html_text = html_text.replace("") + color_span_text = '' + pattern = r"\s*([^<>\s][^<>]*)\s*" + matches = re.findall(pattern, html_text) + + preview_text = "" #TODO + for m in matches: + preview_text = GapFillSource.getPreviewText(m) + html_text = html_text.replace(f"{m}", color_span_text + preview_text + "") + + + html_text = html_text.replace("", color_span_text) + html_text = html_text.replace("", "") self.template_preview.set_html(html_text) update_preview() diff --git a/main.py b/main.py index 13cdaf1..dfb9ab2 100644 --- a/main.py +++ b/main.py @@ -37,6 +37,13 @@ def pushQueuedInstances(): for o in IModel.updateQueued: db.Update(o) IModel.updateQueued.remove(o) + if len(IModel.retrieveAdditionalQueued) > 0: + for o in IModel.retrieveAdditionalQueued: + if isinstance(o, Template): + if o.dataimport_id: + di = db.GetData(DataImport, id=o.dataimport_id) + o.dataimport = di[0] + IModel.retrieveAdditionalQueued.remove(o) if __name__ == "__main__": db = DatabaseHandler(dbURL, tables) diff --git a/models.py b/models.py index 68f1111..59a6b69 100644 --- a/models.py +++ b/models.py @@ -15,6 +15,7 @@ class IModel(declarative_base()): run_loading = True addQueued: list[IModel] = [] updateQueued: list[IModel] = [] + retrieveAdditionalQueued: list[IModel] = [] @staticmethod def queueSave(child): @@ -25,6 +26,11 @@ def queueSave(child): def queueToUpdate(child): if not IModel.run_loading: IModel.updateQueued.append(child) + + @staticmethod + def retrieveAdditionalData(child): + if isinstance(child, Template): + IModel.retrieveAdditionalQueued.append(child) class DataImport(IModel): @@ -53,7 +59,7 @@ def getColumnPreview(self) -> dict | None: continue columns = first_row - dataPreviewRow = next(sheet.iter_rows(values_only=True)) + dataPreviewRow = next(sheet.iter_rows(min_row=2, values_only=True)) for idx, c in enumerate(columns): result[c] = dataPreviewRow[idx] return result if len(result) > 0 else None @@ -113,8 +119,8 @@ def __init__(self, **kwargs) -> None: self.id: int = kwargs.pop('_id', None) self.name: str = kwargs.pop('_name', None) self.content: object = kwargs.pop('_content', None) - self._dataimport: DataImport = None - self.dataimport: DataImport = kwargs.pop("_dataimport_id", None) + self.dataimport: DataImport = None + self.dataimport_id: int = kwargs.pop("_dataimport_id", None) Template.all_instances.append(self) IModel.queueSave(child=self) @@ -139,8 +145,8 @@ def content(self): return self._content @hybrid_property - def dataimport(self) -> DataImport: - return self._dataimport + def dataimport_id(self) -> DataImport: + return self._dataimport_id @id.setter def id(self, newValue: int): @@ -157,12 +163,11 @@ def content(self, value: str | None): self._content = value IModel.queueToUpdate(self) - @dataimport.setter - def dataimport(self, value: DataImport | None): - self._dataimport = value - if value != None: - self._dataimport_id = value.id - IModel.queueToUpdate(self) + @dataimport_id.setter + def dataimport_id(self, value: int | None): + self._dataimport_id = value + IModel.queueToUpdate(self) + IModel.retrieveAdditionalData(self) #endregion From db8229f26f937677249cbc997e1634c051f34842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Wed, 29 May 2024 02:01:23 +0200 Subject: [PATCH 13/20] =?UTF-8?q?=C5=81adowanie=20prawdziwych=20User=C3=B3?= =?UTF-8?q?w=20do=20Settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Interface/Settings.py | 9 +++++---- requirements.txt | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Interface/Settings.py b/Interface/Settings.py index 5ae04fe..ce6d00b 100644 --- a/Interface/Settings.py +++ b/Interface/Settings.py @@ -8,7 +8,7 @@ from tkinter.ttk import Combobox from tkinter.constants import NORMAL, DISABLED, BOTH, RIDGE, END, LEFT, RIGHT, TOP, X, Y, INSERT, SEL, WORD from group_controller import GroupController -from models import Contact, IModel, Template, Group +from models import Contact, IModel, Template, Group, User from tkhtmlview import HTMLLabel, HTMLText from DataSources.dataSources import GapFillSource from MessagingService.accountInfo import discover_email_settings @@ -22,8 +22,9 @@ def __init__(self, root): self.root.geometry("400x400") def prepareInterface(self): - # TODO: tutaj powinniśmy ładować wartości z User - example_emails = ["kuczynskimaciej1@poczta.onet.pl", "example1@example.com", "example2@example.com", "example3@example.com"] + created_users = [] + for u in User.all_instances: + created_users.append(u._email) label = Label( self.root, @@ -31,7 +32,7 @@ def prepareInterface(self): bg="lightblue", font=("Helvetica", 24)) - self.email_combobox = Combobox(self.root, values=example_emails) + self.email_combobox = Combobox(self.root, values=created_users) self.password_entry = Entry(self.root, show="*") diff --git a/requirements.txt b/requirements.txt index 87b4a8a..0b3e161 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,4 @@ sqlalchemy pytest faker tkhtmlview -dnspython -openpyxl \ No newline at end of file +dnspython \ No newline at end of file From 41d5cd5f36ff94abb63a8df18043c3d874fd9949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Wed, 29 May 2024 02:27:39 +0200 Subject: [PATCH 14/20] Dodanie wyprowadzenia zaznaczonych Grup i Templatek z AppUI do SMTPSendera --- Interface/AppUI.py | 22 ++++++++++++++++++---- MessagingService/senders.py | 12 ++++++++++++ main.py | 3 ++- models.py | 12 ++++++++++-- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/Interface/AppUI.py b/Interface/AppUI.py index 547fafa..5129baa 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -4,11 +4,12 @@ from typing import NoReturn from tkinter import Menu, simpledialog, Listbox, Tk, Frame, Label, Entry, Scrollbar from tkinter.constants import BOTH, RIDGE, END, LEFT, RIGHT, TOP, X, Y, INSERT -from models import IModel, Template, Group +from models import IModel, Message, Template, Group, User from tkhtmlview import HTMLLabel from .GroupEditor import GroupEditor from .Settings import Settings from .TemplateEditor import TemplateEditor +from ..MessagingService.senders import ISender def errorHandler(xd, exctype: type, excvalue: Exception, tb: TracebackType): @@ -47,6 +48,8 @@ def populateInterface(self) -> None: for (modelType, ui_func) in modelType_func_mapper.items(): ui_func(modelType.all_instances) + def setSender(self, new_sender: ISender): + self.sender = new_sender def add_periodic_task(self, period: int, func: Callable): # TODO można poprawić żeby się odpalało tylko przy dodaniu obiektu, @@ -96,9 +99,20 @@ def show_group_window(self, g: Group | None = None): group_editor = GroupEditor(self, g) group_editor.prepareInterface() - def __send_clicked(event) -> None: - print("send mail") - pass + def __send_clicked(self, event) -> None: + tmp = self.grupy_listbox.curselection() + if len(tmp) == 0: + raise ValueError("Wybierz grupę!") + else: + selectedGroup: Group = tmp[0] + + tmp = self.template_listbox.curselection() + if len(tmp) == 0: + raise ValueError("Wybierz templatkę!") + else: + selectedTemplate: Template = tmp[0] + + self.sender.SendEmails(selectedGroup, selectedTemplate, User.GetCurrentUser()) def __template_selection_changed(self, _event): selected = self.template_listbox.curselection() diff --git a/MessagingService/senders.py b/MessagingService/senders.py index c8d375c..bc528aa 100644 --- a/MessagingService/senders.py +++ b/MessagingService/senders.py @@ -1,6 +1,8 @@ from abc import ABCMeta, abstractmethod from smtplib import * +from models import Group, Template, User + class ISender(metaclass=ABCMeta): @abstractmethod def Send(self) -> None: @@ -12,9 +14,19 @@ def __subclasshook__(cls, __subclass: type) -> bool: if any("Send" in B.__dict__ for B in __subclass.__mro__): return True return NotImplemented + + @abstractmethod + def SendEmails(self, g: Group, t: Template, u: User) -> None: + # TODO: Tworzenie obiektów Message i wysyłka + raise AssertionError class SMTPSender(ISender): + + def SendEmails(self, g: Group, t: Template, u: User) -> None: + # TODO: Tworzenie obiektów Message i wysyłka + raise NotImplementedError + def Send() -> None: smtp_host = "" #hostname smtp_port = 123 diff --git a/main.py b/main.py index dfb9ab2..3da166b 100644 --- a/main.py +++ b/main.py @@ -57,6 +57,7 @@ def pushQueuedInstances(): ui.prepareInterface() + _contact_fields = GapFillSource() if (mocking_enabled): @@ -68,7 +69,7 @@ def pushQueuedInstances(): sender = SMTPSender(mock_user) except Exception as e: print(e) - + ui.setSender(sender) ui.add_periodic_task(5000, pushQueuedInstances) ui.run() diff --git a/models.py b/models.py index 59a6b69..46963f1 100644 --- a/models.py +++ b/models.py @@ -1,7 +1,7 @@ from __future__ import annotations from email.mime.multipart import MIMEMultipart from openpyxl import load_workbook -from sqlalchemy import Column, ForeignKey, Integer, String, LargeBinary, TIMESTAMP, func +from sqlalchemy import BOOLEAN, Column, ForeignKey, Integer, String, LargeBinary, TIMESTAMP, func from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.ext.hybrid import hybrid_property import re @@ -277,7 +277,7 @@ class User(IModel): _id = Column("_id", Integer, primary_key=True, autoincrement=True) _email = Column("email", String(100), ForeignKey('Contacts.email'), unique=True) - + _selected = Column("selected", BOOLEAN) contactRel = relationship(Contact, foreign_keys=[_email]) @@ -286,8 +286,16 @@ def __init__(self, **kwargs) -> None: self._email = kwargs.pop("_email") self.password = kwargs.pop("_password", None) self.contact = self.getExistingContact(kwargs.pop("_first_name", None), kwargs.pop("_last_name", None)) + self._selected = kwargs.pop("_selected", None) User.all_instances.append(self) IModel.queueSave(child=self) + + @staticmethod + def GetCurrentUser() -> User | None: + for u in User.all_instances: + if u._selected: + return u + return None def getExistingContact(self, first_name, last_name) -> Contact: for c in Contact.all_instances: From 1dfb90b78cbf08e0842d7b4909b1bed7b7de7ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Wed, 29 May 2024 02:34:07 +0200 Subject: [PATCH 15/20] drobne poprawki --- Interface/AppUI.py | 7 ++++--- main.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Interface/AppUI.py b/Interface/AppUI.py index 5129baa..034246d 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -9,7 +9,7 @@ from .GroupEditor import GroupEditor from .Settings import Settings from .TemplateEditor import TemplateEditor -from ..MessagingService.senders import ISender +from MessagingService.senders import ISender def errorHandler(xd, exctype: type, excvalue: Exception, tb: TracebackType): @@ -99,7 +99,8 @@ def show_group_window(self, g: Group | None = None): group_editor = GroupEditor(self, g) group_editor.prepareInterface() - def __send_clicked(self, event) -> None: + def __send_clicked(self) -> None: + # TODO: Jakoś trzeba ogarnąć multiple selection na template + group (albo zrobić jakiś hackment) tmp = self.grupy_listbox.curselection() if len(tmp) == 0: raise ValueError("Wybierz grupę!") @@ -173,7 +174,7 @@ def __create_menu(self): edit_menu.add_cascade(label="Add...", menu=add_menu) menubar.add_cascade(label="Edit", menu=edit_menu) menubar.add_command(label="Open Settings", command=self.__openSettings_clicked) - menubar.add_command(label="Send", command=lambda: self.__send_clicked()) + menubar.add_command(label="Send", command=self.__send_clicked) self.root.config(menu=menubar) diff --git a/main.py b/main.py index 3da166b..f0e3d7c 100644 --- a/main.py +++ b/main.py @@ -66,7 +66,7 @@ def pushQueuedInstances(): _first_name=mock_name, _last_name=mock_lastname, _password=mock_pwd) - sender = SMTPSender(mock_user) + sender = SMTPSender() except Exception as e: print(e) ui.setSender(sender) From ad46c5e9db9cd24614545d92a73087aafd69aaec Mon Sep 17 00:00:00 2001 From: Maciej Kuczynski Date: Wed, 29 May 2024 03:44:21 +0200 Subject: [PATCH 16/20] Default server settings hardcoded --- MessagingService/accountInfo.py | 56 ++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/MessagingService/accountInfo.py b/MessagingService/accountInfo.py index c27ade9..0a0754a 100644 --- a/MessagingService/accountInfo.py +++ b/MessagingService/accountInfo.py @@ -7,12 +7,46 @@ default_settings = { 'gmail.com': { - 'imap': {'host': 'imap.gmail.com', 'port': 993, 'ssl': True}, - 'smtp': {'host': 'smtp.gmail.com', 'port': 587, 'tls': True} + 'imap': {'hostname': 'imap.gmail.com', 'port': 993, 'socket_type': 'SSL'}, + 'smtp': {'hostname': 'smtp.gmail.com', 'port': 587, 'socket_type': 'STARTTLS'} + }, + 'yahoo.com': { + 'imap': {'hostname': 'imap.mail.yahoo.com', 'port': 993, 'socket_type': 'SSL'}, + 'smtp': {'hostname': 'smtp.mail.yahoo.com', 'port': 465, 'socket_type': 'SSL'} + }, + 'outlook.com': { + 'imap': {'hostname': 'outlook.office365.com', 'port': 993, 'socket_type': 'SSL'}, + 'smtp': {'hostname': 'smtp.office365.com', 'port': 587, 'socket_type': 'STARTTLS'} + }, + 'poczta.onet.pl': { + 'imap': {'hostname': 'imap.poczta.onet.pl', 'port': 993, 'socket_type': 'SSL'}, + 'smtp': {'hostname': 'smtp.poczta.onet.pl', 'port': 587, 'socket_type': 'STARTTLS'} + }, + 'onet.pl': { + 'imap': {'hostname': 'imap.poczta.onet.pl', 'port': 993, 'socket_type': 'SSL'}, + 'smtp': {'hostname': 'smtp.poczta.onet.pl', 'port': 465, 'socket_type': 'SSL'} + }, + 'wp.pl': { + 'imap': {'hostname': 'imap.wp.pl', 'port': 993, 'socket_type': 'SSL'}, + 'smtp': {'hostname': 'smtp.wp.pl', 'port': 465, 'socket_type': 'SSL'} + }, + 'interia.pl': { + 'imap': {'hostname': 'imap.poczta.interia.pl', 'port': 993, 'socket_type': 'SSL'}, + 'smtp': {'hostname': 'smtp.poczta.interia.pl', 'port': 465, 'socket_type': 'SSL'} + }, + 'pcz.pl': { + 'imap': {'hostname': 'imap.pcz.pl', 'port': 993, 'socket_type': 'SSL'}, + 'smtp': {'hostname': 'smtp.pcz.pl', 'port': 465, 'socket_type': 'SSL'} + }, + 'wimii.pcz.pl': { + 'imap': {'hostname': 'imap.wimii.pcz.pl', 'port': 993, 'socket_type': 'SSL'}, + 'smtp': {'hostname': 'smtp.wimii.pcz.pl', 'port': 465, 'socket_type': 'SSL'} } } + + def get_domain(email): return email.split('@')[1] @@ -91,21 +125,20 @@ def test_imap_connection(imap_settings, email, password): def test_smtp_connection(smtp_settings, email, password): - for setting in smtp_settings: try: - if setting['socket_type'] == 'SSL': - connection = smtplib.SMTP_SSL(setting['hostname'], setting['port']) + if smtp_settings == 'SSL': + connection = smtplib.SMTP_SSL(smtp_settings['hostname'], smtp_settings['port']) else: - connection = smtplib.SMTP(setting['hostname'], setting['port']) - if setting['socket_type'] == 'STARTTLS': + connection = smtplib.SMTP(smtp_settings['hostname'], smtp_settings['port']) + if smtp_settings == 'STARTTLS': connection.starttls() connection.login(email, password) connection.quit() return True except Exception as e: - print(f"SMTP connection to {setting['hostname']} on port {setting['port']} failed: {e}") - return False + print(f"SMTP connection to {smtp_settings['hostname']} on port {smtp_settings['port']} failed: {e}") + return False def discover_email_settings(email, password): @@ -124,7 +157,8 @@ def discover_email_settings(email, password): # Use default settings if domain in default_settings: - settings = default_settings[domain] + settings_xml = default_settings[domain] + print(settings_xml) else: print("No settings found for this domain.") @@ -135,4 +169,4 @@ def discover_email_settings(email, password): return settings_xml else: print("Failed to connect with discovered settings.") - return None + return None \ No newline at end of file From f050be9a2b7c5e0b31a6ca147a6ea82719175ed4 Mon Sep 17 00:00:00 2001 From: Maciej Kuczynski Date: Wed, 29 May 2024 08:12:31 +0200 Subject: [PATCH 17/20] Server settings --- Interface/AppUI.py | 33 ++++++++++++++++------------- Interface/Settings.py | 16 ++++++++++---- MessagingService/accountInfo.py | 29 +++++++++++++------------ MessagingService/ethereal_demo.py | 35 +++++++++++++++++++++++++++++++ MessagingService/senders.py | 20 ++++++++++++------ MessagingService/smtp_data.py | 9 ++++++++ SMTPAutomationWithLogin.py | 2 +- main.py | 10 ++++----- 8 files changed, 110 insertions(+), 44 deletions(-) create mode 100644 MessagingService/ethereal_demo.py create mode 100644 MessagingService/smtp_data.py diff --git a/Interface/AppUI.py b/Interface/AppUI.py index 034246d..eb9b066 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -10,6 +10,8 @@ from .Settings import Settings from .TemplateEditor import TemplateEditor from MessagingService.senders import ISender +import MessagingService.smtp_data +from MessagingService.ethereal_demo import send_email def errorHandler(xd, exctype: type, excvalue: Exception, tb: TracebackType): @@ -101,19 +103,24 @@ def show_group_window(self, g: Group | None = None): def __send_clicked(self) -> None: # TODO: Jakoś trzeba ogarnąć multiple selection na template + group (albo zrobić jakiś hackment) - tmp = self.grupy_listbox.curselection() - if len(tmp) == 0: - raise ValueError("Wybierz grupę!") - else: - selectedGroup: Group = tmp[0] + #tmp = self.grupy_listbox.curselection() + #if len(tmp) == 0: + # raise ValueError("Wybierz grupę!") + #else: + # selectedGroup: Group = tmp[0] - tmp = self.template_listbox.curselection() - if len(tmp) == 0: - raise ValueError("Wybierz templatkę!") - else: - selectedTemplate: Template = tmp[0] + #tmp = self.template_listbox.curselection() + #if len(tmp) == 0: + # raise ValueError("Wybierz templatkę!") + #else: + # selectedTemplate: Template = tmp[0] - self.sender.SendEmails(selectedGroup, selectedTemplate, User.GetCurrentUser()) + #self.sender.SendEmails(selectedGroup, selectedTemplate, User.GetCurrentUser()) + message = "Hello" + print(message) + #recipient = 'kuczynskimaciej1@poczta.onet.pl' + #self.sender.Send(self, MessagingService.smtp_data.smtp_host, MessagingService.smtp_data.smtp_port, MessagingService.smtp_data.email, MessagingService.smtp_data.password, message, recipient) + send_email() def __template_selection_changed(self, _event): selected = self.template_listbox.curselection() @@ -155,9 +162,7 @@ def __update_listbox(lb: Listbox, content: Iterable[IModel] | dict[IModel]): lb.delete(0, END) [lb.insert(END, k) for k in content.keys()] else: - raise AttributeError( - f"Wrong type of 'content', expected dict or Iterable, got { - type(content)}") + raise AttributeError(f"Wrong type of 'content', expected dict or Iterable, got {type(content)}") def __add_template_clicked(self): self.show_template_window() diff --git a/Interface/Settings.py b/Interface/Settings.py index ce6d00b..6f0fc84 100644 --- a/Interface/Settings.py +++ b/Interface/Settings.py @@ -12,6 +12,7 @@ from tkhtmlview import HTMLLabel, HTMLText from DataSources.dataSources import GapFillSource from MessagingService.accountInfo import discover_email_settings +import MessagingService.smtp_data class Settings: @@ -65,12 +66,19 @@ def prepareInterface(self): close_button.pack(pady=5) def connect(self): - email = self.email_combobox.get() - password = self.password_entry.get() + MessagingService.smtp_data.email = self.email_combobox.get() + MessagingService.smtp_data.password = self.password_entry.get() # TODO: połączenie z pocztą - email_settings = discover_email_settings(email,password) - messagebox.showinfo("Połączenie", f"Połączono z {email}") + email_settings = discover_email_settings(MessagingService.smtp_data.email, MessagingService.smtp_data.password) + print(email_settings) + MessagingService.smtp_data.smtp_host = email_settings['smtp']['hostname'] + print(MessagingService.smtp_data.smtp_host) + MessagingService.smtp_data.smtp_port = email_settings['smtp']['port'] + print(MessagingService.smtp_data.smtp_port) + MessagingService.smtp_data.smtp_security = email_settings['smtp']['socket_type'] + print(MessagingService.smtp_data.smtp_security) + messagebox.showinfo("Połączenie", f"Połączono z {MessagingService.smtp_data.email}") def change_email(self): new_email = simpledialog.askstring( diff --git a/MessagingService/accountInfo.py b/MessagingService/accountInfo.py index 0a0754a..3a8d0f5 100644 --- a/MessagingService/accountInfo.py +++ b/MessagingService/accountInfo.py @@ -8,7 +8,7 @@ default_settings = { 'gmail.com': { 'imap': {'hostname': 'imap.gmail.com', 'port': 993, 'socket_type': 'SSL'}, - 'smtp': {'hostname': 'smtp.gmail.com', 'port': 587, 'socket_type': 'STARTTLS'} + 'smtp': {'hostname': 'smtp.gmail.com', 'port': 465, 'socket_type': 'SSL'} }, 'yahoo.com': { 'imap': {'hostname': 'imap.mail.yahoo.com', 'port': 993, 'socket_type': 'SSL'}, @@ -20,7 +20,7 @@ }, 'poczta.onet.pl': { 'imap': {'hostname': 'imap.poczta.onet.pl', 'port': 993, 'socket_type': 'SSL'}, - 'smtp': {'hostname': 'smtp.poczta.onet.pl', 'port': 587, 'socket_type': 'STARTTLS'} + 'smtp': {'hostname': 'smtp.poczta.onet.pl', 'port': 465, 'socket_type': 'SSL'} }, 'onet.pl': { 'imap': {'hostname': 'imap.poczta.onet.pl', 'port': 993, 'socket_type': 'SSL'}, @@ -41,6 +41,11 @@ 'wimii.pcz.pl': { 'imap': {'hostname': 'imap.wimii.pcz.pl', 'port': 993, 'socket_type': 'SSL'}, 'smtp': {'hostname': 'smtp.wimii.pcz.pl', 'port': 465, 'socket_type': 'SSL'} + }, + + 'ethereal.email': { + 'imap': {'hostname': 'imap.ethereal.email', 'port': 993, 'socket_type': 'SSL'}, + 'smtp': {'hostname': 'smtp.ethereal.email', 'port': 587, 'socket_type': 'STARTTLS'} } } @@ -111,7 +116,7 @@ def parse_email_settings(xml_data): def test_imap_connection(imap_settings, email, password): try: - if imap_settings['socket_type'] == 'SSL': + if imap_settings == 'SSL': connection = imaplib.IMAP4_SSL(imap_settings['hostname'], imap_settings['port']) else: connection = imaplib.IMAP4(imap_settings['hostname'], imap_settings['port']) @@ -149,24 +154,22 @@ def discover_email_settings(email, password): if mx_records: pass - # Try autodiscovery settings_xml = get_autodiscover_settings(domain) if settings_xml: settings_xml = parse_email_settings(settings_xml) pass - # Use default settings if domain in default_settings: settings_xml = default_settings[domain] print(settings_xml) + return settings_xml else: print("No settings found for this domain.") - # Test IMAP and SMTP connections - if test_imap_connection(settings_xml['imap'], email, password) and test_smtp_connection(settings_xml['smtp'], email, password): - print("Check ok") - print(settings_xml) - return settings_xml - else: - print("Failed to connect with discovered settings.") - return None \ No newline at end of file + #if test_imap_connection(settings_xml['imap'], email, password) and test_smtp_connection(settings_xml['smtp'], email, password): + #print("Check ok") + #print(settings_xml) + #return settings_xml + #else: + #print("Failed to connect with discovered settings.") + #return None \ No newline at end of file diff --git a/MessagingService/ethereal_demo.py b/MessagingService/ethereal_demo.py new file mode 100644 index 0000000..7e33fde --- /dev/null +++ b/MessagingService/ethereal_demo.py @@ -0,0 +1,35 @@ +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.mime.application import MIMEApplication +import MessagingService.smtp_data + + +# Ethereal credentials +ETH_USER = "russ.connelly30@ethereal.email" +ETH_PASSWORD = "QQcGx1RmfVkaEMjzqZ" + +def send_email(): + # Email content + sender_email = ETH_USER + receiver_email = "kuczynskimaciej1@poczta.onet.pl" + subject = "Example Email from Python" + body = "Hello." + + # Constructing the email + message = MIMEMultipart() + message["From"] = sender_email + message["To"] = receiver_email + message["Subject"] = subject + message.attach(MIMEText(body, "plain")) + + # Connecting to Ethereal SMTP server + with smtplib.SMTP("smtp.ethereal.email", 587) as server: + server.starttls() + server.login(ETH_USER, ETH_PASSWORD) + server.sendmail(sender_email, receiver_email, message.as_string()) + + print("Email sent successfully!") + +if __name__ == "__main__": + send_email() diff --git a/MessagingService/senders.py b/MessagingService/senders.py index bc528aa..98aac3d 100644 --- a/MessagingService/senders.py +++ b/MessagingService/senders.py @@ -1,7 +1,8 @@ from abc import ABCMeta, abstractmethod from smtplib import * +import MessagingService.smtp_data -from models import Group, Template, User +from models import Group, Template, User, Message class ISender(metaclass=ABCMeta): @abstractmethod @@ -27,13 +28,20 @@ def SendEmails(self, g: Group, t: Template, u: User) -> None: # TODO: Tworzenie obiektów Message i wysyłka raise NotImplementedError - def Send() -> None: - smtp_host = "" #hostname - smtp_port = 123 - server = SMTP_SSL(smtp_host, smtp_port) + def Send(self, host, port, email, password, message, recipient) -> None: + smtp_host = host + smtp_port = port + print("PASSWORD: " + password) + print("RECIPIENT: " + recipient) + print("HOST: " + str(smtp_host)) + print("PORT: " + str(smtp_port)) + server = SMTP(smtp_host, smtp_port) server.connect(smtp_host, smtp_port) + server.starttls() server.ehlo() - server.login() + server.login(email, password) + server.sendmail(email, recipient, message) + server.quit() # class MockSMTPSender(ISender): # def __init__(self) -> None: diff --git a/MessagingService/smtp_data.py b/MessagingService/smtp_data.py new file mode 100644 index 0000000..eb514f6 --- /dev/null +++ b/MessagingService/smtp_data.py @@ -0,0 +1,9 @@ +global smtp_host, smtp_port, smtp_security, email, password + +smtp_host = "smtp.ethereal.email" +smtp_port = 587 +smtp_security = "tls" +email = "" +password = "" +group = None +message = None \ No newline at end of file diff --git a/SMTPAutomationWithLogin.py b/SMTPAutomationWithLogin.py index 2ce330e..7848ecd 100644 --- a/SMTPAutomationWithLogin.py +++ b/SMTPAutomationWithLogin.py @@ -68,7 +68,7 @@ def modifyCell(CELL): def prepareMail(FROM, RECIPIENT, NAME, JOB, ATTACHMENT_PATH): MESSAGE = MIMEMultipart('mixed') - MESSAGE['Subject'] = "JoinThe.Space - networking offer" + MESSAGE['Subject'] = "Subject" MESSAGE['From'] = FROM MESSAGE['To'] = RECIPIENT[0] HTML = """\ diff --git a/main.py b/main.py index f0e3d7c..0153803 100644 --- a/main.py +++ b/main.py @@ -8,18 +8,14 @@ from Interface.AppUI import AppUI from DataSources.dataSources import DatabaseHandler, GapFillSource, IDataSource from additionalTableSetup import GroupContacts, MessageAttachment, SendAttempt +from MessagingService.smtp_data import smtp_security, smtp_host, smtp_port - -mocking_enabled = True +mocking_enabled = False mock_name = "Russ" mock_lastname = "Connelly" mock_login = "russ.connelly30@ethereal.email" mock_pwd = "QQcGx1RmfVkaEMjzqZ" -smtp_host = "smtp.ethereal.email" -smtp_port = 587 -smtp_security = "tls" - dbname = "localSQLite.sqlite3" @@ -69,6 +65,8 @@ def pushQueuedInstances(): sender = SMTPSender() except Exception as e: print(e) + + sender = SMTPSender() ui.setSender(sender) ui.add_periodic_task(5000, pushQueuedInstances) From bbf0924b205ea5c9e546639cf71ebe5a89b99a5a Mon Sep 17 00:00:00 2001 From: Maciej Kuczynski Date: Fri, 31 May 2024 18:53:06 +0200 Subject: [PATCH 18/20] Fixed sending via smtp_ssl --- MessagingService/ethereal_demo.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MessagingService/ethereal_demo.py b/MessagingService/ethereal_demo.py index 7e33fde..e590c7e 100644 --- a/MessagingService/ethereal_demo.py +++ b/MessagingService/ethereal_demo.py @@ -2,17 +2,17 @@ from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication -import MessagingService.smtp_data +#import MessagingService.smtp_data # Ethereal credentials -ETH_USER = "russ.connelly30@ethereal.email" -ETH_PASSWORD = "QQcGx1RmfVkaEMjzqZ" +ETH_USER = "" +ETH_PASSWORD = "" def send_email(): # Email content sender_email = ETH_USER - receiver_email = "kuczynskimaciej1@poczta.onet.pl" + receiver_email = "" subject = "Example Email from Python" body = "Hello." @@ -24,8 +24,8 @@ def send_email(): message.attach(MIMEText(body, "plain")) # Connecting to Ethereal SMTP server - with smtplib.SMTP("smtp.ethereal.email", 587) as server: - server.starttls() + with smtplib.SMTP_SSL("smtp.poczta.onet.pl", 465) as server: + #server.starttls() server.login(ETH_USER, ETH_PASSWORD) server.sendmail(sender_email, receiver_email, message.as_string()) From 7ad940f9f124a20049a6f57833105cb15b19556e Mon Sep 17 00:00:00 2001 From: kuczynskimaciej1 Date: Fri, 31 May 2024 19:26:13 +0200 Subject: [PATCH 19/20] self.currentTemplate = obj if obj is not None else Template() --- Interface/TemplateEditor.py | 4 ++-- grupy.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 grupy.txt diff --git a/Interface/TemplateEditor.py b/Interface/TemplateEditor.py index 0412502..a11d758 100644 --- a/Interface/TemplateEditor.py +++ b/Interface/TemplateEditor.py @@ -16,7 +16,7 @@ def __init__(self, parent: Toplevel | Tk, master: Misc, obj: Template | None = N super().__init__(master) self.parent = parent self.combo_frame: Frame = None - self.currentTemplate = obj + self.currentTemplate = obj if obj is not None else Template() if self.currentTemplate and self.currentTemplate.dataimport: GapFillSource(self.currentTemplate.dataimport) @@ -75,7 +75,7 @@ def update(self): def __on_html_key_clicked(self, event: Event): - if event.keycode not in NonAlteringKeyCodes: + if event.keycode not in [c.value for c in NonAlteringKeyCodes]: self.template_text.event_generate("<>") diff --git a/grupy.txt b/grupy.txt new file mode 100644 index 0000000..78e7b20 --- /dev/null +++ b/grupy.txt @@ -0,0 +1 @@ +1:kh@onet.pl, mk@gmail.com From 89e3da1e3a2fc7dfe071b24f48da5f6d8770bd00 Mon Sep 17 00:00:00 2001 From: kuczynskimaciej1 Date: Fri, 31 May 2024 19:27:39 +0200 Subject: [PATCH 20/20] Fixed bugs - Python 3.11 --- Interface/TemplateEditor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Interface/TemplateEditor.py b/Interface/TemplateEditor.py index a11d758..a040cbc 100644 --- a/Interface/TemplateEditor.py +++ b/Interface/TemplateEditor.py @@ -16,7 +16,7 @@ def __init__(self, parent: Toplevel | Tk, master: Misc, obj: Template | None = N super().__init__(master) self.parent = parent self.combo_frame: Frame = None - self.currentTemplate = obj if obj is not None else Template() + self.currentTemplate = obj if obj is not None else Template() #fix if self.currentTemplate and self.currentTemplate.dataimport: GapFillSource(self.currentTemplate.dataimport) @@ -75,7 +75,7 @@ def update(self): def __on_html_key_clicked(self, event: Event): - if event.keycode not in [c.value for c in NonAlteringKeyCodes]: + if event.keycode not in [c.value for c in NonAlteringKeyCodes]: #python 3.11 fix self.template_text.event_generate("<>")