diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2b7e46d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.formatting.provider": "yapf" +} \ No newline at end of file diff --git a/client/.vscode/launch.json b/client/.vscode/launch.json new file mode 100644 index 0000000..f366b25 --- /dev/null +++ b/client/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: 当前文件", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "gevent": true + } + ] +} \ No newline at end of file diff --git a/client/.vscode/settings.json b/client/.vscode/settings.json new file mode 100644 index 0000000..2b7e46d --- /dev/null +++ b/client/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.formatting.provider": "yapf" +} \ No newline at end of file diff --git a/client/client.py b/client/client.py new file mode 100644 index 0000000..eb94c87 --- /dev/null +++ b/client/client.py @@ -0,0 +1,77 @@ +import threading +import os +import sys +from socket import * +from PyQt5.Qt import * +from main_window_ui import Ui_MainWindow + + +class MainWindow(QMainWindow, Ui_MainWindow): + def __init__(self): + super(MainWindow, self).__init__() + self.setupUi(self) + self.btnSend.clicked.connect(self.btnSend_onclicked) + self.btnConnect.clicked.connect(self.btnConnect_onclicked) + self.statusbar.showMessage("Disconnected") + self.socket: socket = None + self.isConnected = False + + def closeEvent(self, event): + if self.isConnected: + self.btnConnect.click() + self.close() + + def btnConnect_onclicked(self): + if not self.isConnected: + server_addr = self.txtServerInput.text() + sp = server_addr.rfind(':') + if sp in [-1, 0, len(server_addr) - 1]: + QMessageBox.critical(self, "Error", "Invalid address") + return + addr, port = server_addr[:sp], server_addr[sp + 1:] + try: + port = int(port) + except: + QMessageBox.critical(self, "Error", "Invalid port") + return + try: + self.socket = socket(AF_INET, SOCK_STREAM) + self.socket.connect((addr, port)) + except: + QMessageBox.critical(self, "Error", "Connect failed") + return + self.btnConnect.setText('Disconnect') + self.statusbar.showMessage("Connected") + self.isConnected = True + threading.Thread(target=self.recvmsg_loop).start() + else: + self.socket.close() + self.btnConnect.setText('Connect') + self.statusbar.showMessage("Disonnected") + self.isConnected = False + + def btnSend_onclicked(self): + msg = ':'.join([self.txtIDInput.text(), self.txtNewMsg.toPlainText()]) + self.txtNewMsg.clear() + self.socket.send(msg.encode()) + + def recvmsg_loop(self): + while True: + if not self.isConnected: + return + msg = self.socket.recv(1 << 20).decode() + print(msg) + sp1 = msg.find(':') + sp2 = msg.find(':', sp1 + 1) + msg = "{}@{}:\n{}".format(msg[sp1 + 1:sp2], msg[:sp1], + msg[sp2 + 1:]) + self.txtList.append(msg) + self.txtList.moveCursor(QTextCursor.End) + + +if __name__ == "__main__": + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + app = QApplication(sys.argv) + mw = MainWindow() + mw.show() + app.exec() diff --git a/client/compile_ui.cmd b/client/compile_ui.cmd new file mode 100644 index 0000000..c296161 --- /dev/null +++ b/client/compile_ui.cmd @@ -0,0 +1,3 @@ +@echo off +pushd %~dp0 +for %%i in (*.ui) do pyuic5 "%%i" -o "%%~ni_ui.py" \ No newline at end of file diff --git a/client/compile_ui_watch.mjs b/client/compile_ui_watch.mjs new file mode 100644 index 0000000..8ff2a38 --- /dev/null +++ b/client/compile_ui_watch.mjs @@ -0,0 +1,12 @@ +import { readdir, watchFile } from "fs"; +import { extname } from 'path'; +import { exec } from 'child_process'; +readdir('.', (_, files) => { + files.filter(file => extname(file) == '.ui').forEach(file => { + console.log(`Setup modify hook for ${file}`); + watchFile(file, (curr, _) => { + console.log(`${file} modified at ${curr.mtime}.`); + exec('compile_ui'); + }); + }) +}); \ No newline at end of file diff --git a/client/main_window.ui b/client/main_window.ui new file mode 100644 index 0000000..0309092 --- /dev/null +++ b/client/main_window.ui @@ -0,0 +1,86 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + + + + Server: + + + + + + + 127.0.0.1:34567 + + + + + + + Connect + + + + + + + + + + + + + + + + + ID: + + + + + + + + + + Send + + + + + + + + + + + 0 + 0 + 800 + 22 + + + + + + + + diff --git a/client/main_window_ui.py b/client/main_window_ui.py new file mode 100644 index 0000000..87b28d2 --- /dev/null +++ b/client/main_window_ui.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'main_window.ui' +# +# Created by: PyQt5 UI code generator 5.13.2 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(800, 600) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) + self.verticalLayout.setObjectName("verticalLayout") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.label = QtWidgets.QLabel(self.centralwidget) + self.label.setObjectName("label") + self.horizontalLayout.addWidget(self.label) + self.txtServerInput = QtWidgets.QLineEdit(self.centralwidget) + self.txtServerInput.setObjectName("txtServerInput") + self.horizontalLayout.addWidget(self.txtServerInput) + self.btnConnect = QtWidgets.QPushButton(self.centralwidget) + self.btnConnect.setObjectName("btnConnect") + self.horizontalLayout.addWidget(self.btnConnect) + self.verticalLayout.addLayout(self.horizontalLayout) + self.txtList = QtWidgets.QTextBrowser(self.centralwidget) + self.txtList.setObjectName("txtList") + self.verticalLayout.addWidget(self.txtList) + self.txtNewMsg = QtWidgets.QPlainTextEdit(self.centralwidget) + self.txtNewMsg.setObjectName("txtNewMsg") + self.verticalLayout.addWidget(self.txtNewMsg) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.label_2 = QtWidgets.QLabel(self.centralwidget) + self.label_2.setObjectName("label_2") + self.horizontalLayout_2.addWidget(self.label_2) + self.txtIDInput = QtWidgets.QLineEdit(self.centralwidget) + self.txtIDInput.setObjectName("txtIDInput") + self.horizontalLayout_2.addWidget(self.txtIDInput) + self.btnSend = QtWidgets.QPushButton(self.centralwidget) + self.btnSend.setObjectName("btnSend") + self.horizontalLayout_2.addWidget(self.btnSend) + self.verticalLayout.addLayout(self.horizontalLayout_2) + self.verticalLayout.setStretch(1, 7) + self.verticalLayout.setStretch(2, 1) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 22)) + self.menubar.setObjectName("menubar") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.label.setText(_translate("MainWindow", "Server:")) + self.txtServerInput.setText(_translate("MainWindow", "127.0.0.1:34567")) + self.btnConnect.setText(_translate("MainWindow", "Connect")) + self.label_2.setText(_translate("MainWindow", "ID:")) + self.btnSend.setText(_translate("MainWindow", "Send")) diff --git a/mail/.vscode/settings.json b/mail/.vscode/settings.json new file mode 100644 index 0000000..2b7e46d --- /dev/null +++ b/mail/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.formatting.provider": "yapf" +} \ No newline at end of file diff --git a/mail/compile_ui.cmd b/mail/compile_ui.cmd new file mode 100644 index 0000000..c296161 --- /dev/null +++ b/mail/compile_ui.cmd @@ -0,0 +1,3 @@ +@echo off +pushd %~dp0 +for %%i in (*.ui) do pyuic5 "%%i" -o "%%~ni_ui.py" \ No newline at end of file diff --git a/mail/compile_ui_watch.mjs b/mail/compile_ui_watch.mjs new file mode 100644 index 0000000..8ff2a38 --- /dev/null +++ b/mail/compile_ui_watch.mjs @@ -0,0 +1,12 @@ +import { readdir, watchFile } from "fs"; +import { extname } from 'path'; +import { exec } from 'child_process'; +readdir('.', (_, files) => { + files.filter(file => extname(file) == '.ui').forEach(file => { + console.log(`Setup modify hook for ${file}`); + watchFile(file, (curr, _) => { + console.log(`${file} modified at ${curr.mtime}.`); + exec('compile_ui'); + }); + }) +}); \ No newline at end of file diff --git a/mail/log_window.ui b/mail/log_window.ui new file mode 100644 index 0000000..a8dd077 --- /dev/null +++ b/mail/log_window.ui @@ -0,0 +1,24 @@ + + + Dialog + + + + 0 + 0 + 400 + 300 + + + + SendLog + + + + + + + + + + diff --git a/mail/log_window_ui.py b/mail/log_window_ui.py new file mode 100644 index 0000000..f0fb4a8 --- /dev/null +++ b/mail/log_window_ui.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'log_window.ui' +# +# Created by: PyQt5 UI code generator 5.13.2 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(400, 300) + self.gridLayout = QtWidgets.QGridLayout(Dialog) + self.gridLayout.setObjectName("gridLayout") + self.textBrowser = QtWidgets.QTextBrowser(Dialog) + self.textBrowser.setObjectName("textBrowser") + self.gridLayout.addWidget(self.textBrowser, 0, 0, 1, 1) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "SendLog")) diff --git a/mail/mail.py b/mail/mail.py new file mode 100644 index 0000000..ab6c52f --- /dev/null +++ b/mail/mail.py @@ -0,0 +1,90 @@ +import sys +import re +import threading +from base64 import b64encode +from typing import * +from socket import * +from PyQt5.Qt import * +from main_window_ui import Ui_MainWindow +from log_window_ui import Ui_Dialog + + +def static_vars(**kwargs): + def decorate(func): + for k in kwargs: + setattr(func, k, kwargs[k]) + return func + + return decorate + + +@static_vars( + matcher=re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)")) +def validate_email(email: str): + return validate_email.matcher.fullmatch(email) + + +class MainWindow(QMainWindow, Ui_MainWindow): + def __init__(self): + super(MainWindow, self).__init__() + self.setupUi(self) + self.btnSendMail.clicked.connect(self.send_mail) + self.txtFrom.textChanged.connect(self.mail_changed) + + def mail_changed(self): + pass + + def send_mail(self): + servaddr = self.txtServer.text() + username = self.txtFrom.text() + password = self.txtPassword.text() + receiver = self.txtTo.text() + subjects = self.txtSubject.text() + contents = self.txtContent.toPlainText() + if not validate_email(username) or not validate_email(receiver): + QMessageBox.critical(self, "Invalid address", "Invalid From or To") + return + lw = LogWindow( + (servaddr, username, password, receiver, subjects, contents)) + lw.exec() + + +class LogWindow(QDialog, Ui_Dialog): + def __init__(self, data): + super(LogWindow, self).__init__() + self.setupUi(self) + threading.Thread(target=self.do_update_status, args=(data, )).start() + + def do_update_status(self, data): + (servaddr, username, password, receiver, subjects, contents) = data + ss = socket(AF_INET, SOCK_STREAM) + try: + ss.connect((servaddr, 25)) + except Exception: + QMessageBox.critical(self, "Connection Failed", + "Failed connecting smtp server") + return + + def comm(msg: str): + msg += "\r\n" + ss.sendall(msg.encode()) + self.textBrowser.append("C: " + msg) + self.textBrowser.append("S: " + ss.recv(1 << 20).decode()) + + comm("HELO " + servaddr) + comm("AUTH LOGIN") + comm(b64encode(username.encode()).decode()) + comm(b64encode(password.encode()).decode()) + comm("MAIL FROM:<{}>".format(username)) + comm("RCPT TO:<{}>".format(receiver)) + comm("DATA") + comm('\r\n'.join(["Subject: " + subjects, "", contents, "."])) + comm("QUIT") + + +if __name__ == "__main__": + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + app = QApplication(sys.argv) + mw = MainWindow() + mw.show() + app.exec() \ No newline at end of file diff --git a/mail/main_window.ui b/mail/main_window.ui new file mode 100644 index 0000000..524d535 --- /dev/null +++ b/mail/main_window.ui @@ -0,0 +1,129 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + SimpleMailSender + + + + + + + + + + + Server: + + + + + + + From: + + + + + + + Password: + + + + + + + To: + + + + + + + Subject: + + + + + + + + + + + true + + + + + + + true + + + + + + + QLineEdit::PasswordEchoOnEdit + + + true + + + + + + + true + + + + + + + true + + + + + + + + + + + + + + Send + + + + + + + + + 0 + 0 + 800 + 22 + + + + + + + + diff --git a/mail/main_window_ui.py b/mail/main_window_ui.py new file mode 100644 index 0000000..2c01160 --- /dev/null +++ b/mail/main_window_ui.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'main_window.ui' +# +# Created by: PyQt5 UI code generator 5.13.2 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(800, 600) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.centralwidget) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + self.label_5 = QtWidgets.QLabel(self.centralwidget) + self.label_5.setObjectName("label_5") + self.verticalLayout.addWidget(self.label_5) + self.label = QtWidgets.QLabel(self.centralwidget) + self.label.setObjectName("label") + self.verticalLayout.addWidget(self.label) + self.label_2 = QtWidgets.QLabel(self.centralwidget) + self.label_2.setObjectName("label_2") + self.verticalLayout.addWidget(self.label_2) + self.label_3 = QtWidgets.QLabel(self.centralwidget) + self.label_3.setObjectName("label_3") + self.verticalLayout.addWidget(self.label_3) + self.label_4 = QtWidgets.QLabel(self.centralwidget) + self.label_4.setObjectName("label_4") + self.verticalLayout.addWidget(self.label_4) + self.horizontalLayout.addLayout(self.verticalLayout) + self.verticalLayout_2 = QtWidgets.QVBoxLayout() + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.txtServer = QtWidgets.QLineEdit(self.centralwidget) + self.txtServer.setClearButtonEnabled(True) + self.txtServer.setObjectName("txtServer") + self.verticalLayout_2.addWidget(self.txtServer) + self.txtFrom = QtWidgets.QLineEdit(self.centralwidget) + self.txtFrom.setClearButtonEnabled(True) + self.txtFrom.setObjectName("txtFrom") + self.verticalLayout_2.addWidget(self.txtFrom) + self.txtPassword = QtWidgets.QLineEdit(self.centralwidget) + self.txtPassword.setEchoMode(QtWidgets.QLineEdit.PasswordEchoOnEdit) + self.txtPassword.setClearButtonEnabled(True) + self.txtPassword.setObjectName("txtPassword") + self.verticalLayout_2.addWidget(self.txtPassword) + self.txtTo = QtWidgets.QLineEdit(self.centralwidget) + self.txtTo.setClearButtonEnabled(True) + self.txtTo.setObjectName("txtTo") + self.verticalLayout_2.addWidget(self.txtTo) + self.txtSubject = QtWidgets.QLineEdit(self.centralwidget) + self.txtSubject.setClearButtonEnabled(True) + self.txtSubject.setObjectName("txtSubject") + self.verticalLayout_2.addWidget(self.txtSubject) + self.horizontalLayout.addLayout(self.verticalLayout_2) + self.verticalLayout_3.addLayout(self.horizontalLayout) + self.txtContent = QtWidgets.QPlainTextEdit(self.centralwidget) + self.txtContent.setObjectName("txtContent") + self.verticalLayout_3.addWidget(self.txtContent) + self.btnSendMail = QtWidgets.QPushButton(self.centralwidget) + self.btnSendMail.setObjectName("btnSendMail") + self.verticalLayout_3.addWidget(self.btnSendMail) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 22)) + self.menubar.setObjectName("menubar") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "SimpleMailSender")) + self.label_5.setText(_translate("MainWindow", "Server:")) + self.label.setText(_translate("MainWindow", "From:")) + self.label_2.setText(_translate("MainWindow", "Password:")) + self.label_3.setText(_translate("MainWindow", "To:")) + self.label_4.setText(_translate("MainWindow", "Subject:")) + self.btnSendMail.setText(_translate("MainWindow", "Send")) diff --git a/server/server.py b/server/server.py new file mode 100644 index 0000000..2245054 --- /dev/null +++ b/server/server.py @@ -0,0 +1,38 @@ +from gevent import monkey, spawn +monkey.patch_all() +from socket import * +from typing import * + +conns: Set[Tuple[socket, Any]] = set() + + +def boardcast(message: str): + print(message) + for conn, addr in conns: + print('send to ', conn, addr) + conn.send(message.encode()) + + +def process_connection(conn: socket, addr): + conns.add((conn, addr)) + boardcast('server:global:{} connected.'.format(addr)) + while True: + try: + res_raw = conn.recv(1 << 20) + if not res_raw: + raise + except Exception: + conns.remove((conn, addr)) + boardcast('server:global:{} disconnected.'.format(addr)) + return + res = res_raw.decode() + boardcast('{}:{}'.format(addr, res)) + + +if __name__ == "__main__": + with socket(AF_INET, SOCK_STREAM) as s: + s.bind(('0.0.0.0', 34567)) + s.listen(1 << 16) + while True: + conn, addr = s.accept() + spawn(process_connection, conn, addr)