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
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
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)