diff --git a/DataSources/dataSourceAbs.py b/DataSources/dataSourceAbs.py new file mode 100644 index 0000000..c9c83ae --- /dev/null +++ b/DataSources/dataSourceAbs.py @@ -0,0 +1,29 @@ +from abc import abstractmethod + + +class IDataSource(): + current_instance = None + + @classmethod + def __subclasshook__(cls, subclass: type) -> bool: + return (hasattr(subclass, 'GetData') and + callable(subclass.GetData) and + hasattr(subclass, 'checkIntegrity') and + callable(subclass.checkIntegrity) and + hasattr(subclass, 'LoadSavedState') and + callable(subclass.LoadSavedState) + ) + + @abstractmethod + def GetData(self): + raise RuntimeError + + @abstractmethod + def checkIntegrity(self): + raise RuntimeError + + @abstractmethod + def LoadSavedState(self): + """Collect all data saved in data source and instantiate adjacent model objects + """ + raise RuntimeError \ No newline at end of file diff --git a/DataSources/dataSources.py b/DataSources/dataSources.py index b969e25..6164db5 100644 --- a/DataSources/dataSources.py +++ b/DataSources/dataSources.py @@ -1,10 +1,11 @@ from __future__ import annotations -from abc import ABCMeta, abstractmethod from collections.abc import Iterable from enum import Enum +from .dataSourceAbs import IDataSource from pandas import read_csv, read_excel, DataFrame from additionalTableSetup import GroupContacts -from models import DataImport, IModel, Contact +from group_controller import GroupController +from models import DataImport, Group, IModel, Contact, Template import sqlalchemy as alchem import sqlalchemy.orm as orm from sqlalchemy.exc import IntegrityError @@ -13,32 +14,6 @@ class SupportedDbEngines(Enum): SQLite=1 -class IDataSource(): - @classmethod - def __subclasshook__(cls, subclass: type) -> bool: - return (hasattr(subclass, 'GetData') and - callable(subclass.GetData) and - hasattr(subclass, 'checkIntegrity') and - callable(subclass.checkIntegrity) and - hasattr(subclass, 'LoadSavedState') and - callable(subclass.LoadSavedState) - ) - - @abstractmethod - def GetData(self): - raise RuntimeError - - @abstractmethod - def checkIntegrity(self): - raise RuntimeError - - @abstractmethod - def LoadSavedState(self): - """Collect all data saved in data source and instantiate adjacent model objects - """ - raise RuntimeError - - class DatabaseHandler(IDataSource): def __init__(self, connectionString: str, tableCreators: Iterable[IModel], engine: SupportedDbEngines = SupportedDbEngines.SQLite) -> None: @@ -51,6 +26,7 @@ def __init__(self, connectionString: str, tableCreators: Iterable[IModel], case _: raise NotImplementedError self.tableCreators = tableCreators + IDataSource.current_instance = self def checkIntegrity(self) -> bool: @@ -116,6 +92,20 @@ def LoadSavedState(self) -> None: print(e) continue IModel.run_loading = False + self.runAdditionalBindings() + + + def runAdditionalBindings(self): + for g in Group.all_instances: + g.contacts = GroupController.get_contacts(g) + + for t in Template.all_instances: + if t.dataimport_id != None: + for di in DataImport.all_instances: + if t.dataimport_id == di.id: + t.dataimport = di + break # TODO w razie dodanie większej ilości di - templatek + def Update(self, obj: IModel): Session = orm.sessionmaker(bind=self.dbEngineInstance) @@ -162,7 +152,6 @@ def GetData(self) -> DataFrame: print("Błąd podczas wczytywania pliku XLSX:", e) return None - class CSVHandler(IDataSource): def __init__(self, path: str) -> None: self.file_path = path @@ -185,9 +174,10 @@ class GapFillSource(): all_instances: list[GapFillSource] = [] def __init__(self, source: IDataSource | IModel = Contact) -> None: - if isinstance(source, IDataSource): - self.iData: IDataSource = source - elif isinstance(source, DataImport) or isinstance(source, list): + # if isinstance(source, IDataSource): + # self.iData: IDataSource = source + # el + if isinstance(source, DataImport) or isinstance(source, list): self.model_source: IModel = source elif issubclass(source, IModel): self.model_source: IModel = source @@ -198,25 +188,28 @@ def __init__(self, source: IDataSource | IModel = Contact) -> None: self.get_possible_values() def get_possible_values(self): - if hasattr(self, "iData"): - idata_type = type(self.iData) - match(idata_type): - case type(DatabaseHandler): - # openTablePicker() - pass - case type(XLSXHandler): - # openSheetPicker() - pass - case type(CSVHandler): - # openCsvPicker() - pass - elif hasattr(self, "model_source"): + # if hasattr(self, "iData"): + # idata_type = type(self.iData) + # match(idata_type): + # case type(DatabaseHandler): + # # openTablePicker() + # pass + # case type(XLSXHandler): + # # openSheetPicker() + # pass + # case type(CSVHandler): + # # openCsvPicker() + # pass + # el + if 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") + else: + raise AttributeError(f"Incorrectly created GapFillSource, expected 'model_source'={self.model_source} to be present.") @staticmethod def getPreviewText(searched: str) -> str | None: diff --git a/Interface/AddContactWindow.py b/Interface/AddContactWindow.py index 8648394..9a54a0e 100644 --- a/Interface/AddContactWindow.py +++ b/Interface/AddContactWindow.py @@ -1,16 +1,5 @@ -from collections.abc import Callable, Iterable -from enum import Enum -from sqlalchemy.exc import IntegrityError -from types import TracebackType -from traceback import print_tb -from typing import Literal, Any, NoReturn -from tkinter import Event, Menu, simpledialog, ttk, Listbox, Tk, Text, Button, Frame, Label, Entry, Scrollbar, Toplevel, Misc, messagebox, Menubutton, Canvas,Checkbutton,BooleanVar, VERTICAL, RAISED -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 tkhtmlview import HTMLLabel, HTMLText -from DataSources.dataSources import GapFillSource +from tkinter import Button, Label, Entry, Toplevel, messagebox +from models import Contact class AddContactWindow(Toplevel): diff --git a/Interface/AppUI.py b/Interface/AppUI.py index eb9b066..49014e7 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -2,16 +2,16 @@ from types import TracebackType from traceback import print_tb 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 tkinter import Menu, simpledialog, Listbox, Tk, Frame, Label, Entry, Scrollbar, Button, messagebox +from tkinter.constants import BOTH, RIDGE, END, LEFT, RIGHT, TOP, BOTTOM, X, Y, INSERT 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 -import MessagingService.smtp_data -from MessagingService.ethereal_demo import send_email +#import MessagingService.smtp_data +#from MessagingService.ethereal_demo import send_email def errorHandler(xd, exctype: type, excvalue: Exception, tb: TracebackType): @@ -42,17 +42,25 @@ def prepareInterface(self) -> None: def populateInterface(self) -> None: - modelType_func_mapper = { - Template: self.add_template, - Group: self.add_group - } + self.update_templates() + self.update_groups() + # modelType_func_mapper = { + # Group: self.update_groups + # } - for (modelType, ui_func) in modelType_func_mapper.items(): - ui_func(modelType.all_instances) + # 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 setUser(self, current_user: User): + self.user = current_user + + def setDb(self, new_db): + #AppUI.db = new_db + IModel.db = new_db + def add_periodic_task(self, period: int, func: Callable): # TODO można poprawić żeby się odpalało tylko przy dodaniu obiektu, # przemyśleć @@ -69,21 +77,12 @@ def __exit_clicked(self) -> NoReturn | None: print("Exiting") exit() - def add_template(self, content: Template | Iterable[Template]): - if isinstance(content, Template): - if content not in self.szablony: - self.szablony.append(content) - else: - [self.szablony.append(i) - for i in content if i not in self.szablony] + def update_templates(self): + self.szablony = Template.all_instances self.__update_listbox(self.template_listbox, self.szablony) - def add_group(self, g: Group | Iterable[Group]): - if isinstance(g, Group): - if g not in self.grupy: - self.grupy.append(g) - else: - [self.grupy.append(i) for i in g if i not in self.grupy] + def update_groups(self): + self.grupy = Group.all_instances self.__update_listbox(self.grupy_listbox, self.grupy) def clearData(self): @@ -102,25 +101,18 @@ def show_group_window(self, g: Group | None = None): group_editor.prepareInterface() 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] + if self.selected_mailing_group == None: + messagebox.showerror("Error", "Wybierz grupę!") + return + + if self.selected_template_group == None: + messagebox.showerror("Error", "Wybierz szablon!") + return - #tmp = self.template_listbox.curselection() - #if len(tmp) == 0: - # raise ValueError("Wybierz templatkę!") - #else: - # selectedTemplate: Template = tmp[0] + u = User.GetCurrentUser() + self.sender.Send(self.selected_mailing_group, self.selected_template_group, u) + messagebox.showinfo("Zakończono wysyłanie", "Ukończono wysyłanie maili") - #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() @@ -196,11 +188,27 @@ def __create_mailing_group_pane(self): '<>', self.__group_selection_changed) self.grupy_listbox.bind('', self.__group_doubleclicked) + assign_button = Button( + groups_frame, text="Wybierz grupę", command=self.__assign_group) groups_frame.pack(side=LEFT, padx=10, pady=10, fill=BOTH, expand=True, ipadx=5, ipady=5) grupy_label.pack() self.grupy_listbox.pack(fill=BOTH, expand=True) + assign_button.pack(side=BOTTOM) + + + def __assign_group(self): + selected_index = self.grupy_listbox.curselection() + if selected_index: + selected_group = self.grupy_listbox.get(selected_index) + nameidx = selected_group.find(": ") + 1 # do ujęcia w tym spacji + selected_group = selected_group[nameidx + 1::] # dodawanie do pominięcia spacji + for g in Group.all_instances: + if g.name == selected_group: + self.selected_mailing_group = g + return + raise LookupError(f"Nie znaleziono grupy {selected_group}") def __create_template_pane(self): @@ -214,11 +222,25 @@ def __create_template_pane(self): '<>', self.__template_selection_changed) self.template_listbox.bind('', self.__template_doubleclicked) + assign_button = Button( + templates_frame, text="Wybierz szablon", command=self.__assign_template) templates_frame.pack(side=LEFT, padx=10, pady=10, fill=BOTH, expand=True, ipadx=5, ipady=5) szablony_label.pack() self.template_listbox.pack(fill=BOTH, expand=True) + assign_button.pack(side=BOTTOM) + + + def __assign_template(self): + selected_index = self.template_listbox.curselection() + if selected_index: + selected_template = self.template_listbox.get(selected_index) + for t in Template.all_instances: + if t.name == selected_template: + self.selected_template_group = t + return + raise LookupError(f"Nie znaleziono szablonu {selected_template}") def __create_mail_input_pane(self): @@ -246,10 +268,9 @@ def show_template_window(self, obj: Template | None = None): self.template_window.prepareInterface() def __openSettings_clicked(self): - root = Tk() # Otwórz ponownie okno logowania - settings = Settings(root) + settings = Settings(self) settings.prepareInterface() - root.mainloop() + # root.mainloop() diff --git a/Interface/ContactList.py b/Interface/ContactList.py index c7f1859..3315977 100644 --- a/Interface/ContactList.py +++ b/Interface/ContactList.py @@ -1,16 +1,8 @@ -from collections.abc import Callable, Iterable -from enum import Enum from sqlalchemy.exc import IntegrityError -from types import TracebackType -from traceback import print_tb -from typing import Literal, Any, NoReturn -from tkinter import Event, Menu, simpledialog, ttk, Listbox, Tk, Text, Button, Frame, Label, Entry, Scrollbar, Toplevel, Misc, messagebox, Menubutton, Canvas,Checkbutton,BooleanVar, VERTICAL, RAISED -from tkinter.ttk import Combobox -from tkinter.constants import NORMAL, DISABLED, BOTH, RIDGE, END, LEFT, RIGHT, TOP, X, Y, INSERT, SEL, WORD +from tkinter import Button, Frame, Label, Entry, Scrollbar, Toplevel, Canvas, Checkbutton, BooleanVar, VERTICAL +from tkinter.constants import BOTH, LEFT, RIGHT, X, Y from group_controller import GroupController -from models import Contact, IModel, Template, Group -from tkhtmlview import HTMLLabel, HTMLText -from DataSources.dataSources import GapFillSource +from models import Contact, Group from .AddContactWindow import AddContactWindow class ContactList(Toplevel): diff --git a/Interface/ExternalSourceImportWindow.py b/Interface/ExternalSourceImportWindow.py index 8f573e2..e5949a3 100644 --- a/Interface/ExternalSourceImportWindow.py +++ b/Interface/ExternalSourceImportWindow.py @@ -88,5 +88,6 @@ def update_preview(self, event=None): def add_data(self): di = DataImport(_name=basename(self.file_path), _localPath=self.file_path) self.template.dataimport = di + self.template.dataimport_id = di.id self.parent.update() self.destroy() diff --git a/Interface/Settings.py b/Interface/Settings.py index 6f0fc84..8ff0c05 100644 --- a/Interface/Settings.py +++ b/Interface/Settings.py @@ -1,58 +1,44 @@ -from collections.abc import Callable, Iterable -from enum import Enum -from sqlalchemy.exc import IntegrityError -from types import TracebackType -from traceback import print_tb -from typing import Literal, Any, NoReturn -from tkinter import Event, Menu, simpledialog, ttk, Listbox, Tk, Text, Button, Frame, Label, Entry, Scrollbar, Toplevel, Misc, messagebox, Menubutton, Canvas,Checkbutton,BooleanVar, VERTICAL, RAISED +from tkinter import simpledialog, Tk, Button, Label, Entry, Toplevel, messagebox 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, User -from tkhtmlview import HTMLLabel, HTMLText -from DataSources.dataSources import GapFillSource -from MessagingService.accountInfo import discover_email_settings -import MessagingService.smtp_data +from models import User -class Settings: - def __init__(self, root): - self.root = root - self.root.title("Ustawienia") - self.root.configure(bg="lightblue") - self.root.geometry("400x400") +class Settings(Toplevel): + def __init__(self, parent: Toplevel | Tk): + super().__init__(parent.root) + self.parent = parent + self.title("Ustawienia") + self.configure(bg="lightblue") + self.geometry("400x400") def prepareInterface(self): - created_users = [] - for u in User.all_instances: - created_users.append(u._email) - label = Label( - self.root, + self, text="MailBuddy", bg="lightblue", font=("Helvetica", 24)) - self.email_combobox = Combobox(self.root, values=created_users) + self.email_combobox = Combobox(self) + + self.password_entry = Entry(self, show="*") - self.password_entry = Entry(self.root, show="*") - connect_button = Button( - self.root, + self, text="Połącz", bg="lightblue", fg="black", command=self.connect) change_email_button = Button( - self.root, + self, text="Dodaj nowy adres mailowy", bg="lightblue", fg="black", command=self.change_email) close_button = Button( - self.root, + self, text="Wyłącz ustawienia", bg="lightblue", fg="black", @@ -64,25 +50,46 @@ def prepareInterface(self): connect_button.pack(pady=5) change_email_button.pack(pady=5) close_button.pack(pady=5) + self.updateCombobox() - def connect(self): - MessagingService.smtp_data.email = self.email_combobox.get() - MessagingService.smtp_data.password = self.password_entry.get() + def updateCombobox(self): + created_users = [] + for u in User.all_instances: + created_users.append(u._email) + + self.email_combobox['values'] = created_users + + def getUser(self): + email = self.email_combobox.get() + password = self.password_entry.get() + + for u in User.all_instances: + if u.email == email and u.password == password: + u.selected = True + u.password = password + return u + return User(_email=email, _password=password, _selected=True) - # TODO: połączenie z pocztą - email_settings = discover_email_settings(MessagingService.smtp_data.email, MessagingService.smtp_data.password) + def connect(self): + user = self.getUser() + + email_settings = user.discover_email_settings() 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}") + user._smtp_host = email_settings['smtp']['hostname'] + print(user._smtp_host) + user._smtp_port = email_settings['smtp']['port'] + print(user._smtp_port) + user._smtp_socket_type = email_settings['smtp']['socket_type'] + print(user._smtp_socket_type) + messagebox.showinfo("Połączenie", f"Połączono z {user._email}") def change_email(self): new_email = simpledialog.askstring( "Zmień adres e-mail", "Dodaj nowy adres e-mail") + if not new_email: + return + + User(_email=new_email) if new_email: self.email_combobox.set(new_email) diff --git a/Interface/TemplateEditor.py b/Interface/TemplateEditor.py index a040cbc..f453683 100644 --- a/Interface/TemplateEditor.py +++ b/Interface/TemplateEditor.py @@ -87,10 +87,12 @@ def update_preview(): 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 + "") + if preview_text: + tmp = html_text.replace(f"{m}", color_span_text + preview_text + "") + if tmp: + html_text = tmp html_text = html_text.replace("", color_span_text) @@ -102,8 +104,9 @@ def update_preview(): 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.currentTemplate.name = template_name + self.currentTemplate.content = template_content + self.parent.update_templates() self.destroy() diff --git a/MessagingService/accountInfo.py b/MessagingService/accountInfo.py index 3a8d0f5..3933ca8 100644 --- a/MessagingService/accountInfo.py +++ b/MessagingService/accountInfo.py @@ -4,54 +4,6 @@ import imaplib import xml.etree.ElementTree as ET - -default_settings = { - 'gmail.com': { - 'imap': {'hostname': 'imap.gmail.com', 'port': 993, 'socket_type': 'SSL'}, - 'smtp': {'hostname': 'smtp.gmail.com', 'port': 465, 'socket_type': 'SSL'} - }, - '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': 465, 'socket_type': 'SSL'} - }, - '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'} - }, - - 'ethereal.email': { - 'imap': {'hostname': 'imap.ethereal.email', 'port': 993, 'socket_type': 'SSL'}, - 'smtp': {'hostname': 'smtp.ethereal.email', 'port': 587, 'socket_type': 'STARTTLS'} - } -} - - - - def get_domain(email): return email.split('@')[1] @@ -166,10 +118,11 @@ def discover_email_settings(email, password): else: print("No settings found for this domain.") - #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/senders.py b/MessagingService/senders.py index 98aac3d..0684f3c 100644 --- a/MessagingService/senders.py +++ b/MessagingService/senders.py @@ -1,7 +1,7 @@ from abc import ABCMeta, abstractmethod +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText from smtplib import * -import MessagingService.smtp_data - from models import Group, Template, User, Message class ISender(metaclass=ABCMeta): @@ -15,33 +15,37 @@ 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(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() + + def Send(self, g: Group, t: Template, u: User) -> None: + # print("PASSWORD: " + u.password) + print("HOST: " + str(u._smtp_host)) + print("PORT: " + str(u._smtp_port)) + if not u._smtp_host or not u._smtp_port: + raise AttributeError("Nie połączyłeś się z serwerem, aby pobrać ustawienia") + server = SMTP_SSL(u._smtp_host, u._smtp_port) + server.connect(u._smtp_host, u._smtp_port) + #server.starttls() server.ehlo() - server.login(email, password) - server.sendmail(email, recipient, message) + server.login(u._email, u.password) + print("logged in successfully") + emailsSent = 0 + print(f"Sending {t.name} to {len(g.contacts)} contacts") + for contact in g.contacts: + print(f"Sending do {contact.email}") + message = Message(t, contact) + mimemsg = MIMEMultipart("alternative") + mimemsg["From"] = u._email + mimemsg["To"] = contact.email + mimemsg["Subject"] = "MailBuddy mailing" # TODO Temat maila w TemplateEditor, aby dało się go parametryzować + xd = MIMEText(message.getParsedBody(), "html") + mimemsg.attach(xd) + server.send_message(mimemsg) + emailsSent += 1 server.quit() + print(f"Sent {emailsSent}") # class MockSMTPSender(ISender): # def __init__(self) -> None: diff --git a/globaldb.py b/globaldb.py new file mode 100644 index 0000000..a33bb82 --- /dev/null +++ b/globaldb.py @@ -0,0 +1,2 @@ +#global db +#db = None \ No newline at end of file diff --git a/group_controller.py b/group_controller.py index 12347ff..8dcf9a8 100644 --- a/group_controller.py +++ b/group_controller.py @@ -1,11 +1,11 @@ from additionalTableSetup import GroupContacts from models import Group, Contact -from DataSources.dataSources import DatabaseHandler +from DataSources.dataSourceAbs import IDataSource class GroupController: - dbh: DatabaseHandler = None + dbh: IDataSource = None @classmethod - def setDbHandler(cls, handler: DatabaseHandler) -> None: + def setDbHandler(cls, handler: IDataSource) -> None: cls.dbh = handler @classmethod diff --git a/main.py b/main.py index 0153803..4029aaa 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,8 @@ 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 +#from MessagingService.smtp_data import smtp_security, smtp_host, smtp_port +#from globaldb import db mocking_enabled = False mock_name = "Russ" @@ -21,26 +22,8 @@ dbname = "localSQLite.sqlite3" dbURL = f"sqlite:///{dbname}" tables = [Template, DataImport, Attachment, Contact, User, ITrigger, Message, Group, MessageAttachment, SendAttempt, GroupContacts] -db: IDataSource = None -def pushQueuedInstances(): - if len(IModel.addQueued) > 0: - for o in IModel.addQueued: - db.Save(o) - IModel.addQueued.remove(o) - if len(IModel.updateQueued) > 0: - 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) GroupController.setDbHandler(db) @@ -51,8 +34,8 @@ def pushQueuedInstances(): ui = AppUI() ui.prepareInterface() - - + + ui.setDb(db) _contact_fields = GapFillSource() @@ -67,7 +50,11 @@ def pushQueuedInstances(): print(e) sender = SMTPSender() - ui.setSender(sender) + ui.setSender(sender) + + # user = + # User(_email="russ.connelly30@ethereal.email", _password="QQcGx1RmfVkaEMjzqZ", _first_name="Russ", _last_name="Connelly", _selected=True) + # ui.setUser(user) - ui.add_periodic_task(5000, pushQueuedInstances) + # ui.add_periodic_task(5000, pushQueuedInstances) ui.run() diff --git a/models.py b/models.py index 46963f1..71a15a8 100644 --- a/models.py +++ b/models.py @@ -1,10 +1,18 @@ from __future__ import annotations from email.mime.multipart import MIMEMultipart from openpyxl import load_workbook +from xml.etree.ElementTree import ParseError 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 +from dns.resolver import resolve +import requests +import smtplib +import imaplib +import xml.etree.ElementTree as ET +#from globaldb import db +#from DataSources.dataSources import IDataSource __all__ = ["Template", "Attachment", "Contact", "User", "Message", "Group"] @@ -16,23 +24,45 @@ class IModel(declarative_base()): addQueued: list[IModel] = [] updateQueued: list[IModel] = [] retrieveAdditionalQueued: list[IModel] = [] + db = None @staticmethod def queueSave(child): if not IModel.run_loading: IModel.addQueued.append(child) + IModel.pushQueuedInstances() @staticmethod def queueToUpdate(child): if not IModel.run_loading: IModel.updateQueued.append(child) + IModel.pushQueuedInstances() @staticmethod def retrieveAdditionalData(child): - if isinstance(child, Template): + if isinstance(child, Template) or isinstance(child, Group): IModel.retrieveAdditionalQueued.append(child) - - + IModel.pushQueuedInstances() + + @staticmethod + def pushQueuedInstances(): + if len(IModel.addQueued) > 0: + for o in IModel.addQueued: + IModel.db.Save(o) + IModel.addQueued.remove(o) + if len(IModel.updateQueued) > 0: + for o in IModel.updateQueued: + IModel.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 = IModel.db.GetData(DataImport, id=o.dataimport_id) + o.dataimport = di[0] + IModel.retrieveAdditionalQueued.remove(o) + + class DataImport(IModel): all_instances: list[DataImport] = [] __tablename__ = "DataImport" @@ -49,6 +79,7 @@ def __init__(self, **kwargs) -> None: self.content = kwargs.pop('_content', None) DataImport.all_instances.append(self) IModel.queueSave(child=self) + print(f"Utworzono {type(self)}") def getColumnPreview(self) -> dict | None: workbook = load_workbook(self.localPath, read_only=True) @@ -63,6 +94,26 @@ def getColumnPreview(self) -> dict | None: for idx, c in enumerate(columns): result[c] = dataPreviewRow[idx] return result if len(result) > 0 else None + + def GetRow(self, email: str) -> dict[str, str]: + with open(self.localPath) as r: + workbook = load_workbook(r, 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 + emailColumnIdx = first_row.index("Email") + + for row in sheet.iter_rows(min_row=2, values_only=True): + if row[emailColumnIdx] == email: + for idx, column in enumerate(first_row): + result[column] = row[idx] + break + break + if len(result) == 0: + raise AttributeError("Nie znaleziono odpowiadającej linijki w pliku z danymi do uzupełnienia") + return result # region Properties @@ -116,6 +167,7 @@ class Template(IModel): # dataImportRel = relationship(DataImport, foreign_keys=[DataImport._id]) def __init__(self, **kwargs) -> None: + self.obj_creation = True self.id: int = kwargs.pop('_id', None) self.name: str = kwargs.pop('_name', None) self.content: object = kwargs.pop('_content', None) @@ -123,6 +175,8 @@ def __init__(self, **kwargs) -> None: self.dataimport_id: int = kwargs.pop("_dataimport_id", None) Template.all_instances.append(self) IModel.queueSave(child=self) + self.obj_creation = False + print(f"Utworzono {type(self)}") def __str__(self) -> str: @@ -130,6 +184,23 @@ def __str__(self) -> str: def __repr__(self): return f"Template(_name={self.name}, _content={self.content}, _id={self.id})" + + def FillGaps(self, fillData: dict[str, str]) -> str: + html_text = self.content + + span_text = '' # TODO korzystać z tej metody do każdego generowania podglądu + pattern = r"\s*([^<>\s][^<>]*)\s*" + matches = re.findall(pattern, html_text) + + for m in matches: + try: + preview_text = fillData[m] + except KeyError as ke: + raise AttributeError("Nie znaleziono odpowiadającej wartości dla luki", ke) + tmp = html_text.replace(f"{m}", span_text + preview_text + "") + if tmp: + html_text = tmp + return html_text # region Properties @hybrid_property @@ -156,17 +227,20 @@ def id(self, newValue: int): @name.setter def name(self, value: str | None): self._name = value - IModel.queueToUpdate(self) + if not self.obj_creation: + IModel.queueToUpdate(self) @content.setter def content(self, value: str | None): self._content = value - IModel.queueToUpdate(self) + if not self.obj_creation: + IModel.queueToUpdate(self) @dataimport_id.setter def dataimport_id(self, value: int | None): self._dataimport_id = value - IModel.queueToUpdate(self) + if not self.obj_creation: + IModel.queueToUpdate(self) IModel.retrieveAdditionalData(self) #endregion @@ -186,6 +260,7 @@ def __init__(self, path, type, attachment_id: int | None = None) -> None: self.type = type Attachment.all_instances.append(self) IModel.queueSave(child=self) + print(f"Utworzono {type(self)}") # def prepareAttachment(self): # att = MIMEApplication(open(self.path, "rb").read(), _subtype=self.type) @@ -216,6 +291,7 @@ def __init__(self, **kwargs) -> None: self.last_name = kwargs.pop("_last_name", "") Contact.all_instances.append(self) IModel.queueSave(child=self) + print(f"Utworzono {type(self)}") def __str__(self) -> str: return f"{self.first_name} {self.last_name}, <{self.email}>" @@ -280,15 +356,206 @@ class User(IModel): _selected = Column("selected", BOOLEAN) contactRel = relationship(Contact, foreign_keys=[_email]) - + default_settings = { + 'gmail.com': { + 'imap': {'hostname': 'imap.gmail.com', 'port': 993, 'socket_type': 'SSL'}, + 'smtp': {'hostname': 'smtp.gmail.com', 'port': 465, 'socket_type': 'SSL'} + }, + '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': 465, 'socket_type': 'SSL'} + }, + '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'} + }, + + 'ethereal.email': { + 'imap': {'hostname': 'imap.ethereal.email', 'port': 993, 'socket_type': 'SSL'}, + 'smtp': {'hostname': 'smtp.ethereal.email', 'port': 587, 'socket_type': 'STARTTLS'} + } +} + def __init__(self, **kwargs) -> None: - self._email = kwargs.pop("_email") + 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) + self.selected = kwargs.pop("_selected", False) + self._smtp_host = "" + self._smtp_port = "" + self._smtp_socket_type = "SSL" User.all_instances.append(self) IModel.queueSave(child=self) + print(f"Utworzono {type(self)}") + +# region Properties + @hybrid_property + def email(self): + return self._email + + @email.setter + def email(self, newValue: str): + self._email = newValue + + @hybrid_property + def selected(self) -> bool: + return self._selected + + @selected.setter + def selected(self, newValue: bool): + if newValue == True: + for u in User.all_instances: + u._selected = False + self._selected = newValue +#endregion + + @staticmethod + def get_domain(email): + return email.split('@')[1] + + @staticmethod + 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 [] + + + @staticmethod + 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 + 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 + except Exception as e: + print(f"Autodiscover failed: {e}") + return None + + + @staticmethod + def parse_email_settings(xml_data): + try: + tree = ET.ElementTree(ET.fromstring(xml_data)) + except ParseError: + raise AttributeError("Zwrócono niepoprawny settings XML") + + 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 == '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): + try: + if smtp_settings == 'SSL': + connection = smtplib.SMTP_SSL(smtp_settings['hostname'], smtp_settings['port']) + else: + 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 {smtp_settings['hostname']} on port {smtp_settings['port']} failed: {e}") + return False + + + def discover_email_settings(self): + domain = User.get_domain(self.email) + settings_xml = User.get_autodiscover_settings(domain) + if not settings_xml: + raise AttributeError("Nie znaleziono opcji dla podanej domeny") + + # try: + settings_xml = User.parse_email_settings(settings_xml) + # except + + if domain in self.default_settings: + settings_xml = self.default_settings[domain] + print(settings_xml) + return settings_xml + + if self.test_imap_connection(settings_xml['imap'], self.email, self.password) \ + and self.test_smtp_connection(settings_xml['smtp'], self.email, self.password): + print("Check ok") + print(settings_xml) + return settings_xml + + raise AttributeError("Nie znaleziono opcji dla podanej domeny") + @staticmethod def GetCurrentUser() -> User | None: @@ -304,7 +571,7 @@ def getExistingContact(self, first_name, last_name) -> Contact: return Contact(_first_name=first_name, _last_name=last_name, _email=self._email) -class Message(IModel, MIMEMultipart): +class Message(IModel): all_instances = [] __tablename__ = "Messages" @@ -319,19 +586,30 @@ class Message(IModel, MIMEMultipart): # contact = relationship("Contact") # template = relationship("Template") - def __init__(self, recipient: Contact, - att: list[Attachment] = None) -> None: + def __init__(self, template: Template, recipient: Contact, + att: list[Attachment] = []) -> None: self.recipient = recipient + self.email = recipient.email self.att = att + self.template = template + self.template_id = template.id Message.all_instances.append(self) IModel.queueSave(child=self) - + print(f"Utworzono {type(self)}") + + def getParsedBody(self) -> str: + data = dict() + if self.template.dataimport_id != None: + data = self.template.dataimport.GetRow(self.email) + body: str = self.template.FillGaps(data) + print(f"Mail: {body}") + return body class Group(IModel): all_instances: list[Group] = [] __tablename__ = "Groups" - _id = Column("id", Integer, primary_key=True) + _id = Column("id", Integer, primary_key=True, autoincrement="auto") _name = Column("name", String(100), nullable=True) def __init__(self, **kwargs): @@ -377,10 +655,7 @@ def name(self): @id.setter def id(self, newValue: int): - if newValue: - self._id = newValue - else: - self._id = max((i.id for i in Group.all_instances), default=-1) + 1 + self._id = newValue @name.setter def name(self, value: str | None):