From fc561d7f4730bc43d8aaab6eb78829fda3ed48bb Mon Sep 17 00:00:00 2001 From: Maciej Kuczynski Date: Fri, 31 May 2024 22:28:10 +0200 Subject: [PATCH 1/7] Added buttons for assigning templates and groups --- Interface/AppUI.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/Interface/AppUI.py b/Interface/AppUI.py index eb9b066..561cd9c 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -2,8 +2,8 @@ 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 +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 @@ -102,7 +102,6 @@ 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ę!") @@ -196,11 +195,21 @@ 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) + self.selected_mailing_group = selected_group def __create_template_pane(self): @@ -214,11 +223,21 @@ 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_group = self.template_listbox.get(selected_index) + self.selected_template_group = selected_group def __create_mail_input_pane(self): From efbba59694e79160386aef41ea16780fd426c96d Mon Sep 17 00:00:00 2001 From: Maciej Kuczynski Date: Sat, 1 Jun 2024 12:11:37 +0200 Subject: [PATCH 2/7] User connection settings --- Interface/AppUI.py | 37 +++---- Interface/Settings.py | 27 +++-- MessagingService/accountInfo.py | 63 ++---------- MessagingService/senders.py | 37 +++---- main.py | 5 +- models.py | 176 ++++++++++++++++++++++++++++++++ 6 files changed, 230 insertions(+), 115 deletions(-) diff --git a/Interface/AppUI.py b/Interface/AppUI.py index 561cd9c..6b50601 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -2,7 +2,7 @@ from types import TracebackType from traceback import print_tb from typing import NoReturn -from tkinter import Menu, simpledialog, Listbox, Tk, Frame, Label, Entry, Scrollbar, Button +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 @@ -10,8 +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 +#import MessagingService.smtp_data +#from MessagingService.ethereal_demo import send_email def errorHandler(xd, exctype: type, excvalue: Exception, tb: TracebackType): @@ -53,6 +53,9 @@ def populateInterface(self) -> None: def setSender(self, new_sender: ISender): self.sender = new_sender + def setUser(self, current_user: User): + self.user = current_user + def add_periodic_task(self, period: int, func: Callable): # TODO można poprawić żeby się odpalało tylko przy dodaniu obiektu, # przemyśleć @@ -102,24 +105,16 @@ def show_group_window(self, g: Group | None = None): group_editor.prepareInterface() def __send_clicked(self) -> 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] + if self.selected_mailing_group == None: + messagebox.showerror("Error", "Wybierz grupę!") + return + + if self.selected_template_group == None: + messagebox.showerror("Error", "Wybierz szablon!") + return - #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() + self.sender.Send(self.selected_mailing_group, self.selected_template_group, self.user) + #send_email() def __template_selection_changed(self, _event): selected = self.template_listbox.curselection() @@ -266,7 +261,7 @@ def show_template_window(self, obj: Template | None = None): def __openSettings_clicked(self): root = Tk() # Otwórz ponownie okno logowania - settings = Settings(root) + settings = Settings(root) #USER settings.prepareInterface() root.mainloop() diff --git a/Interface/Settings.py b/Interface/Settings.py index 6f0fc84..9e79893 100644 --- a/Interface/Settings.py +++ b/Interface/Settings.py @@ -11,8 +11,8 @@ 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 main import ui +#import MessagingService.smtp_data class Settings: @@ -36,7 +36,7 @@ def prepareInterface(self): self.email_combobox = Combobox(self.root, values=created_users) self.password_entry = Entry(self.root, show="*") - + connect_button = Button( self.root, text="Połącz", @@ -66,19 +66,18 @@ def prepareInterface(self): close_button.pack(pady=5) def connect(self): - MessagingService.smtp_data.email = self.email_combobox.get() - MessagingService.smtp_data.password = self.password_entry.get() + User.all_instances._email = self.email_combobox.get() + user.password = self.password_entry.get() - # TODO: połączenie z pocztą - email_settings = discover_email_settings(MessagingService.smtp_data.email, MessagingService.smtp_data.password) + email_settings = user.discover_email_settings(user._email, user._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}") + 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( 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..61801e8 100644 --- a/MessagingService/senders.py +++ b/MessagingService/senders.py @@ -1,7 +1,5 @@ from abc import ABCMeta, abstractmethod from smtplib import * -import MessagingService.smtp_data - from models import Group, Template, User, Message class ISender(metaclass=ABCMeta): @@ -15,32 +13,23 @@ 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)) + 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) + for contact in g.contacts: + recipient = contact._email + message = Message() + server.sendmail(u._email, recipient, message) server.quit() # class MockSMTPSender(ISender): diff --git a/main.py b/main.py index 0153803..cffcb13 100644 --- a/main.py +++ b/main.py @@ -67,7 +67,10 @@ 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.run() diff --git a/models.py b/models.py index 46963f1..0e93dc8 100644 --- a/models.py +++ b/models.py @@ -5,6 +5,11 @@ 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 __all__ = ["Template", "Attachment", "Contact", "User", "Message", "Group"] @@ -280,6 +285,177 @@ 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'} + } +} + + _smtp_host = "" + _smtp_port = "" + _smtp_socket_type = "SSL" + + + 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 + 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 + + + + 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 == '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, email, password): + domain = self.get_domain(email) + + mx_records = self.get_mx_records(domain) + print(mx_records) + if mx_records: + pass + + settings_xml = self.get_autodiscover_settings(domain) + if settings_xml: + settings_xml = self.parse_email_settings(settings_xml) + pass + + if domain in self.default_settings: + settings_xml = self.default_settings[domain] + print(settings_xml) + return settings_xml + else: + print("No settings found for this domain.") + + if self.test_imap_connection(settings_xml['imap'], email, password) and self.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 def __init__(self, **kwargs) -> None: From b8d56fa0453d8e9e6818c64dd4ac7e011b6c6025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Sat, 1 Jun 2024 14:49:59 +0200 Subject: [PATCH 3/7] =?UTF-8?q?Zmiany=20wysy=C5=82ania=20-=20Message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Interface/AppUI.py | 65 ++++++++++------- Interface/Settings.py | 59 ++++++++++----- Interface/TemplateEditor.py | 8 +- MessagingService/senders.py | 13 +++- main.py | 5 +- models.py | 142 ++++++++++++++++++++++++++---------- 6 files changed, 200 insertions(+), 92 deletions(-) diff --git a/Interface/AppUI.py b/Interface/AppUI.py index 6b50601..3e46360 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -42,13 +42,14 @@ 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 @@ -72,21 +73,23 @@ 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): + # 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] + 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): + # 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] + self.grupy = Group.all_instances self.__update_listbox(self.grupy_listbox, self.grupy) def clearData(self): @@ -113,7 +116,8 @@ def __send_clicked(self) -> None: messagebox.showerror("Error", "Wybierz szablon!") return - self.sender.Send(self.selected_mailing_group, self.selected_template_group, self.user) + u = User.GetCurrentUser() + self.sender.Send(self.selected_mailing_group, self.selected_template_group, u) #send_email() def __template_selection_changed(self, _event): @@ -204,7 +208,13 @@ def __assign_group(self): selected_index = self.grupy_listbox.curselection() if selected_index: selected_group = self.grupy_listbox.get(selected_index) - self.selected_mailing_group = selected_group + 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): @@ -231,8 +241,12 @@ def __create_template_pane(self): def __assign_template(self): selected_index = self.template_listbox.curselection() if selected_index: - selected_group = self.template_listbox.get(selected_index) - self.selected_template_group = selected_group + 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): @@ -260,10 +274,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) #USER + settings = Settings(self) settings.prepareInterface() - root.mainloop() + # root.mainloop() diff --git a/Interface/Settings.py b/Interface/Settings.py index 9e79893..6428791 100644 --- a/Interface/Settings.py +++ b/Interface/Settings.py @@ -15,44 +15,41 @@ #import MessagingService.smtp_data -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.root, show="*") + self.password_entry = Entry(self, 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,12 +61,30 @@ def prepareInterface(self): connect_button.pack(pady=5) change_email_button.pack(pady=5) close_button.pack(pady=5) + self.updateCombobox() - def connect(self): - User.all_instances._email = self.email_combobox.get() - user.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) - email_settings = user.discover_email_settings(user._email, user._password) + def connect(self): + user = self.getUser() + + email_settings = user.discover_email_settings() print(email_settings) user._smtp_host = email_settings['smtp']['hostname'] print(user._smtp_host) @@ -82,6 +97,10 @@ def connect(self): 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..51ca9ec 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) @@ -103,7 +105,7 @@ 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.parent.update_templates() self.destroy() diff --git a/MessagingService/senders.py b/MessagingService/senders.py index 61801e8..2b3edda 100644 --- a/MessagingService/senders.py +++ b/MessagingService/senders.py @@ -18,19 +18,24 @@ def __subclasshook__(cls, __subclass: type) -> bool: class SMTPSender(ISender): def Send(self, g: Group, t: Template, u: User) -> None: - print("PASSWORD: " + u.password) + # 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(u._email, u.password) + print("logged in successfully") + emailsSent = 0 for contact in g.contacts: - recipient = contact._email - message = Message() - server.sendmail(u._email, recipient, message) + message = Message(t, contact) + server.sendmail(u._email, contact.email, message.getParsedBody()) + emailsSent += 1 server.quit() + print(f"Sent {emailsSent}") # class MockSMTPSender(ISender): # def __init__(self) -> None: diff --git a/main.py b/main.py index cffcb13..f7b875f 100644 --- a/main.py +++ b/main.py @@ -69,8 +69,9 @@ def pushQueuedInstances(): sender = SMTPSender() ui.setSender(sender) - user = User(_email="russ.connelly30@ethereal.email", _password="QQcGx1RmfVkaEMjzqZ", _first_name="Russ", _last_name="Connelly", _selected=True) - ui.setUser(user) + # 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.run() diff --git a/models.py b/models.py index 0e93dc8..3aec1b3 100644 --- a/models.py +++ b/models.py @@ -1,6 +1,7 @@ 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 @@ -68,6 +69,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]: + 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 + 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 + break + if len(result) == 0: + raise AttributeError("Nie znaleziono odpowiadającej linijki w pliku z danymi do uzupełnienia") + return result # region Properties @@ -135,6 +156,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 @@ -330,15 +368,43 @@ class User(IModel): } } - _smtp_host = "" - _smtp_port = "" - _smtp_socket_type = "SSL" + 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", False) + self._smtp_host = "" + self._smtp_port = "" + self._smtp_socket_type = "SSL" + User.all_instances.append(self) + IModel.queueSave(child=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') @@ -349,6 +415,7 @@ def get_mx_records(domain): return [] + @staticmethod def get_autodiscover_settings(domain): try: url = f'https://autoconfig.{domain}/mail/config-v1.1.xml' @@ -365,9 +432,13 @@ def get_autodiscover_settings(domain): return None - + @staticmethod def parse_email_settings(xml_data): - tree = ET.ElementTree(ET.fromstring(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') @@ -428,43 +499,29 @@ def test_smtp_connection(smtp_settings, email, password): return False - def discover_email_settings(self, email, password): - domain = self.get_domain(email) - - mx_records = self.get_mx_records(domain) - print(mx_records) - if mx_records: - pass + 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") - settings_xml = self.get_autodiscover_settings(domain) - if settings_xml: - settings_xml = self.parse_email_settings(settings_xml) - pass + # 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 - else: - print("No settings found for this domain.") - if self.test_imap_connection(settings_xml['imap'], email, password) and self.test_smtp_connection(settings_xml['smtp'], email, password): - print("Check ok") - 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 - else: - print("Failed to connect with discovered settings.") - return None + raise AttributeError("Nie znaleziono opcji dla podanej domeny") - - 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: @@ -480,7 +537,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" @@ -495,12 +552,23 @@ 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) + + def getParsedBody(self) -> str: + data = dict() + if self.template.dataimport: + data = self.template.dataimport.GetRow(self.email) + body: str = self.template.FillGaps(data) + return body + class Group(IModel): From 8d60f376ccfa5f2935028b80e8ff344447b6ef05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Sat, 1 Jun 2024 17:12:34 +0200 Subject: [PATCH 4/7] stash not working --- DataSources/dataSources.py | 3 +++ Interface/ExternalSourceImportWindow.py | 1 + main.py | 20 ++----------------- models.py | 26 ++++++++++++++++++++++++- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/DataSources/dataSources.py b/DataSources/dataSources.py index b969e25..74a56a9 100644 --- a/DataSources/dataSources.py +++ b/DataSources/dataSources.py @@ -14,6 +14,8 @@ class SupportedDbEngines(Enum): SQLite=1 class IDataSource(): + current_instance = None + @classmethod def __subclasshook__(cls, subclass: type) -> bool: return (hasattr(subclass, 'GetData') and @@ -51,6 +53,7 @@ def __init__(self, connectionString: str, tableCreators: Iterable[IModel], case _: raise NotImplementedError self.tableCreators = tableCreators + IDataSource.current_instance = self def checkIntegrity(self) -> bool: 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/main.py b/main.py index f7b875f..4c0ef5a 100644 --- a/main.py +++ b/main.py @@ -21,26 +21,10 @@ dbname = "localSQLite.sqlite3" dbURL = f"sqlite:///{dbname}" tables = [Template, DataImport, Attachment, Contact, User, ITrigger, Message, Group, MessageAttachment, SendAttempt, GroupContacts] +global db 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) @@ -73,5 +57,5 @@ def pushQueuedInstances(): # 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 3aec1b3..f10e8e0 100644 --- a/models.py +++ b/models.py @@ -27,16 +27,40 @@ class IModel(declarative_base()): 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(): + global db + 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) + + class DataImport(IModel): From 2e00b7924b75568545b3fb10b34f5b48acd7c39f Mon Sep 17 00:00:00 2001 From: Maciej Kuczynski Date: Sat, 1 Jun 2024 22:05:09 +0200 Subject: [PATCH 5/7] Fix in DB global variable --- DataSources/dataSources.py | 1 - Interface/AppUI.py | 4 ++++ MessagingService/senders.py | 3 ++- globaldb.py | 2 ++ main.py | 9 ++++----- models.py | 18 ++++++++++++++---- 6 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 globaldb.py diff --git a/DataSources/dataSources.py b/DataSources/dataSources.py index 74a56a9..57d09b4 100644 --- a/DataSources/dataSources.py +++ b/DataSources/dataSources.py @@ -40,7 +40,6 @@ def LoadSavedState(self): """ raise RuntimeError - class DatabaseHandler(IDataSource): def __init__(self, connectionString: str, tableCreators: Iterable[IModel], engine: SupportedDbEngines = SupportedDbEngines.SQLite) -> None: diff --git a/Interface/AppUI.py b/Interface/AppUI.py index 3e46360..77da5e1 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -57,6 +57,10 @@ def setSender(self, new_sender: ISender): 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ć diff --git a/MessagingService/senders.py b/MessagingService/senders.py index 2b3edda..3f3d5cd 100644 --- a/MessagingService/senders.py +++ b/MessagingService/senders.py @@ -32,7 +32,8 @@ def Send(self, g: Group, t: Template, u: User) -> None: emailsSent = 0 for contact in g.contacts: message = Message(t, contact) - server.sendmail(u._email, contact.email, message.getParsedBody()) + #server.sendmail(u._email, contact.email, message.getParsedBody()) + server.sendmail(u._email, contact.email, message.prepareMail()) emailsSent += 1 server.quit() print(f"Sent {emailsSent}") 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/main.py b/main.py index 4c0ef5a..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,8 +22,6 @@ dbname = "localSQLite.sqlite3" dbURL = f"sqlite:///{dbname}" tables = [Template, DataImport, Attachment, Contact, User, ITrigger, Message, Group, MessageAttachment, SendAttempt, GroupContacts] -global db -db: IDataSource = None if __name__ == "__main__": @@ -35,8 +34,8 @@ ui = AppUI() ui.prepareInterface() - - + + ui.setDb(db) _contact_fields = GapFillSource() diff --git a/models.py b/models.py index f10e8e0..482c88b 100644 --- a/models.py +++ b/models.py @@ -11,6 +11,8 @@ 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"] @@ -22,6 +24,7 @@ class IModel(declarative_base()): addQueued: list[IModel] = [] updateQueued: list[IModel] = [] retrieveAdditionalQueued: list[IModel] = [] + db = None @staticmethod def queueSave(child): @@ -43,20 +46,19 @@ def retrieveAdditionalData(child): @staticmethod def pushQueuedInstances(): - global db if len(IModel.addQueued) > 0: for o in IModel.addQueued: - db.Save(o) + IModel.db.Save(o) IModel.addQueued.remove(o) if len(IModel.updateQueued) > 0: for o in IModel.updateQueued: - db.Update(o) + 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 = db.GetData(DataImport, id=o.dataimport_id) + di = IModel.db.GetData(DataImport, id=o.dataimport_id) o.dataimport = di[0] IModel.retrieveAdditionalQueued.remove(o) @@ -593,6 +595,14 @@ def getParsedBody(self) -> str: body: str = self.template.FillGaps(data) return body + #def prepareMail(self) -> str: + #pathToXlsx = self.template.dataimport._localPath + #print(pathToXlsx) + + #template = self.template._content + + #return body + class Group(IModel): From 5ccc554f11ac2e1a3276b619e04f7280a93e2941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Mon, 3 Jun 2024 19:53:49 +0200 Subject: [PATCH 6/7] =?UTF-8?q?Poprawienie=20wczytywania=20obiekt=C3=B3w?= =?UTF-8?q?=20po=20restarcie,=20usuni=C4=99cie=20niepotrzebnych=20using?= =?UTF-8?q?=C3=B3w,=20wyekstrahowanie=20IDataSource,=20poprawne=20wys?= =?UTF-8?q?=C5=82anie=20maili?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DataSources/dataSourceAbs.py | 29 ++++++++++++ DataSources/dataSources.py | 83 ++++++++++++++++------------------- Interface/AddContactWindow.py | 15 +------ Interface/AppUI.py | 3 +- Interface/ContactList.py | 14 ++---- Interface/Settings.py | 15 +------ MessagingService/senders.py | 13 +++++- group_controller.py | 6 +-- models.py | 17 ++----- 9 files changed, 92 insertions(+), 103 deletions(-) create mode 100644 DataSources/dataSourceAbs.py 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 57d09b4..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,33 +14,6 @@ class SupportedDbEngines(Enum): SQLite=1 -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 - class DatabaseHandler(IDataSource): def __init__(self, connectionString: str, tableCreators: Iterable[IModel], engine: SupportedDbEngines = SupportedDbEngines.SQLite) -> None: @@ -118,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) @@ -164,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 @@ -187,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 @@ -200,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 77da5e1..355a5ee 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -122,7 +122,8 @@ def __send_clicked(self) -> None: u = User.GetCurrentUser() self.sender.Send(self.selected_mailing_group, self.selected_template_group, u) - #send_email() + messagebox.showinfo("Zakończono wysyłanie", "Ukończono wysyłanie maili") + def __template_selection_changed(self, _event): selected = self.template_listbox.curselection() 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/Settings.py b/Interface/Settings.py index 6428791..8ff0c05 100644 --- a/Interface/Settings.py +++ b/Interface/Settings.py @@ -1,18 +1,7 @@ -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 main import ui -#import MessagingService.smtp_data +from models import User class Settings(Toplevel): diff --git a/MessagingService/senders.py b/MessagingService/senders.py index 3f3d5cd..0684f3c 100644 --- a/MessagingService/senders.py +++ b/MessagingService/senders.py @@ -1,4 +1,6 @@ from abc import ABCMeta, abstractmethod +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText from smtplib import * from models import Group, Template, User, Message @@ -30,10 +32,17 @@ def Send(self, g: Group, t: Template, u: User) -> None: 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) - #server.sendmail(u._email, contact.email, message.getParsedBody()) - server.sendmail(u._email, contact.email, message.prepareMail()) + 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}") 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/models.py b/models.py index 482c88b..f78e943 100644 --- a/models.py +++ b/models.py @@ -63,8 +63,6 @@ def pushQueuedInstances(): IModel.retrieveAdditionalQueued.remove(o) - - class DataImport(IModel): all_instances: list[DataImport] = [] __tablename__ = "DataImport" @@ -109,11 +107,11 @@ def GetRow(self, email: str) -> dict[str, str]: if row[emailColumnIdx] == email: for idx, column in enumerate(first_row): result[column] = row[idx] - break break break if len(result) == 0: raise AttributeError("Nie znaleziono odpowiadającej linijki w pliku z danymi do uzupełnienia") + workbook.close() return result @@ -590,20 +588,11 @@ def __init__(self, template: Template, recipient: Contact, def getParsedBody(self) -> str: data = dict() - if self.template.dataimport: + 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 - - #def prepareMail(self) -> str: - #pathToXlsx = self.template.dataimport._localPath - #print(pathToXlsx) - - #template = self.template._content - - #return body - - class Group(IModel): all_instances: list[Group] = [] From 9a20cbd94b27b44c50a484944db510293e294ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Mon, 3 Jun 2024 20:39:52 +0200 Subject: [PATCH 7/7] Poprawienie tworzenia Templatek --- Interface/AppUI.py | 11 ------- Interface/TemplateEditor.py | 3 +- models.py | 58 +++++++++++++++++++++---------------- 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/Interface/AppUI.py b/Interface/AppUI.py index 355a5ee..49014e7 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -78,21 +78,10 @@ def __exit_clicked(self) -> NoReturn | None: exit() def update_templates(self): - # 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] self.szablony = Template.all_instances self.__update_listbox(self.template_listbox, self.szablony) def update_groups(self): - # 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] self.grupy = Group.all_instances self.__update_listbox(self.grupy_listbox, self.grupy) diff --git a/Interface/TemplateEditor.py b/Interface/TemplateEditor.py index 51ca9ec..f453683 100644 --- a/Interface/TemplateEditor.py +++ b/Interface/TemplateEditor.py @@ -104,7 +104,8 @@ 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.currentTemplate.name = template_name + self.currentTemplate.content = template_content self.parent.update_templates() self.destroy() diff --git a/models.py b/models.py index f78e943..71a15a8 100644 --- a/models.py +++ b/models.py @@ -79,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) @@ -95,23 +96,23 @@ def getColumnPreview(self) -> dict | None: return result if len(result) > 0 else None def GetRow(self, email: str) -> dict[str, str]: - 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 - 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") - workbook.close() + 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 @@ -166,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) @@ -173,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: @@ -223,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 @@ -253,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) @@ -283,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}>" @@ -402,6 +411,7 @@ def __init__(self, **kwargs) -> None: self._smtp_socket_type = "SSL" User.all_instances.append(self) IModel.queueSave(child=self) + print(f"Utworzono {type(self)}") # region Properties @hybrid_property @@ -585,6 +595,7 @@ def __init__(self, template: Template, recipient: Contact, 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() @@ -598,7 +609,7 @@ 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): @@ -644,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):