mailbox-tk.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. import tkinter as tk
  2. import tkinter.messagebox as msg
  3. import tkinter.filedialog as fd
  4. import tkinter.scrolledtext as st
  5. import json
  6. import imaplib
  7. from typing import Optional, List
  8. from time import strftime, localtime, strptime
  9. from mailbox.email import Mail
  10. from mailbox.imap import Imap
  11. # 顶层Frame
  12. FRAME_PADX = 5
  13. # 顶层窗口按钮
  14. BUTTON_WIDTH = 40
  15. BUTTON_WIDTH_L = 20
  16. BUTTON_HEIGHT = 1
  17. MAILBOX_WIDTH = 60
  18. MAILBOX_HEIGHT = 5
  19. # 登录框
  20. LOGIN_LABEL_WIDTH = 10
  21. LOGIN_LABEL_HEIGHT = 1
  22. LOGIN_ENTRY_WIDTH = 35
  23. LOGIN_BTN_WIDTH = 20
  24. LOGIN_RBTN_WIDTH = 6
  25. LOGIN_MAILBOX_WIDTH = 45
  26. LOGIN_MAILBOX_HEIGHT = 10
  27. # GET框
  28. GET_LABEL_WIDTH = 10
  29. GET_LABEL_HEIGHT = 1
  30. GET_ENTRY_WIDTH = 35
  31. # refresh框
  32. REFRESH_LABEL_WIDTH = 10
  33. REFRESH_LABEL_HEIGHT = 1
  34. REFRESH_ENTRY_WIDTH = 35
  35. # 邮件信息显示框
  36. EMAIL_INFO_BUTTON_WIDTH = 20
  37. EMAIL_INFO_BUTTON_HEIGHT = 1
  38. EMAIL_INFO_WIDTH = 100
  39. EMAIL_INFO_HEIGHT = 40
  40. class MailboxGUI(tk.Tk):
  41. def __init__(self):
  42. super().__init__()
  43. self.imap: Optional[Imap] = None
  44. self.iconbitmap('static/HuanMail.ico')
  45. self.title("HuanMail (IMAP)")
  46. self.resizable(False, False)
  47. self.btn_frame = tk.Frame(self)
  48. self.mailbox_frame = tk.Frame(self)
  49. self.btn_frame.grid(column=0, row=0)
  50. self.mailbox_frame.grid(column=1, row=0)
  51. self.mailbox_num: List[str] = []
  52. self.mailbox_x = tk.Scrollbar(self.mailbox_frame, orient=tk.HORIZONTAL)
  53. self.mailbox_y = tk.Scrollbar(self.mailbox_frame)
  54. self.mailbox = tk.Listbox(self.mailbox_frame,
  55. xscrollcommand=self.mailbox_x.set, yscrollcommand=self.mailbox_y.set,
  56. width=MAILBOX_WIDTH, height=MAILBOX_HEIGHT)
  57. self.mailbox_x.config(command=self.mailbox.xview)
  58. self.mailbox_y.config(command=self.mailbox.yview)
  59. self.mailbox.grid(column=0, row=0, ipady=38)
  60. self.mailbox_x.grid(column=0, row=1, sticky="NEWS")
  61. self.mailbox_y.grid(column=1, row=0, sticky="NEWS")
  62. self.login_frame = tk.Frame(self.btn_frame)
  63. self.mailbox_frame = tk.Frame(self.btn_frame)
  64. self.check_frame = tk.Frame(self.btn_frame)
  65. self.config_frame = tk.Frame(self.btn_frame)
  66. self.quit_frame = tk.Frame(self.btn_frame)
  67. for i, frame in enumerate([self.login_frame, self.mailbox_frame, self.check_frame,
  68. self.config_frame, self.quit_frame]):
  69. frame.grid(column=0, row=i, padx=FRAME_PADX)
  70. button_arg = dict(width=BUTTON_WIDTH_L, height=BUTTON_HEIGHT, border=2)
  71. button_big_arg = dict(width=BUTTON_WIDTH, height=BUTTON_HEIGHT, border=2)
  72. self.login = tk.Button(self.login_frame, text="Login", command=self.show_login, **button_arg)
  73. self.info = tk.Button(self.login_frame, text="Info", command=self.show_info, **button_arg)
  74. self.get = tk.Button(self.mailbox_frame, text="Get", command=self.show_get, **button_arg)
  75. self.refresh = tk.Button(self.mailbox_frame, text="Refresh", command=self.show_refresh, **button_arg)
  76. self.check = tk.Button(self.check_frame, text="Check", command=self.show_email, **button_big_arg)
  77. self.check.pack(side="top", pady=2.5, ipadx=8)
  78. self.load = tk.Button(self.config_frame, text="Load", command=self.load_config, **button_arg)
  79. self.save = tk.Button(self.config_frame, text="Save", command=self.save_config, **button_arg)
  80. for i in (self.login, self.info, self.get, self.refresh, self.load, self.save):
  81. i.pack(side="left", pady=2.5, ipadx=2)
  82. self.quit_btn = tk.Button(self.quit_frame, text="Quit", command=self.destroy, **button_big_arg)
  83. self.quit_btn.pack(side="top", pady=2.5, ipadx=8)
  84. def show_login(self):
  85. win = tk.Toplevel(self)
  86. win.title("User Login")
  87. win.resizable(False, False)
  88. label_arg = dict(width=LOGIN_LABEL_WIDTH, height=LOGIN_LABEL_HEIGHT)
  89. entry_arg = dict(width=LOGIN_ENTRY_WIDTH)
  90. host_frame = tk.Frame(win)
  91. host_label = tk.Label(host_frame, text="Host: ", **label_arg)
  92. host = tk.Entry(host_frame, **entry_arg)
  93. host_label.pack(side="left")
  94. host.pack(side="left")
  95. host_frame.pack(side="top", pady=2, padx=5)
  96. port_frame = tk.Frame(win)
  97. port_label = tk.Label(port_frame, text="Port: ", **label_arg)
  98. port = tk.Entry(port_frame, **entry_arg)
  99. port_label.pack(side="left")
  100. port.pack(side="left")
  101. port_frame.pack(side="top", pady=2, padx=5)
  102. user_frame = tk.Frame(win)
  103. user_label = tk.Label(user_frame, text="User: ", **label_arg)
  104. user = tk.Entry(user_frame, **entry_arg)
  105. user_label.pack(side="left")
  106. user.pack(side="left")
  107. user_frame.pack(side="top", pady=2, padx=5)
  108. passwd_frame = tk.Frame(win)
  109. passwd_label = tk.Label(passwd_frame, text="Passwd: ", **label_arg)
  110. passwd = tk.Entry(passwd_frame, **entry_arg)
  111. passwd_label.pack(side="left")
  112. passwd.pack(side="left")
  113. passwd_frame.pack(side="top", pady=2, padx=5)
  114. ssl_frame = tk.Frame(win)
  115. ssl_label = tk.Label(ssl_frame, text="SSL: ", **label_arg)
  116. ssl = tk.IntVar(ssl_frame)
  117. ssl.set(1)
  118. ssl_btn = (tk.Radiobutton(ssl_frame, variable=ssl, value=1, text="No", width=LOGIN_RBTN_WIDTH),
  119. tk.Radiobutton(ssl_frame, variable=ssl, value=2, text="SSL", width=LOGIN_RBTN_WIDTH),
  120. tk.Radiobutton(ssl_frame, variable=ssl, value=3, text="StartSSL", width=LOGIN_RBTN_WIDTH))
  121. ssl_label.pack(side="left")
  122. for i in ssl_btn:
  123. i.pack(side="left")
  124. ssl_frame.pack(side="top", pady=2, padx=5)
  125. login_btn = tk.Button(win, text="Login", width=LOGIN_BTN_WIDTH,
  126. command=lambda: self.login_user(host.get(), port.get(), user.get(), passwd.get(),
  127. ssl.get(), mailbox_list))
  128. login_btn.pack(side="top", pady=2)
  129. mailbox_list = tk.Listbox(win, width=LOGIN_MAILBOX_WIDTH, height=LOGIN_MAILBOX_HEIGHT)
  130. mailbox_list.pack(side="top", pady=(20, 2), padx=5)
  131. self.refresh_mailbox(mailbox_list)
  132. select_btn = tk.Button(win, text="Select", width=LOGIN_BTN_WIDTH,
  133. command=lambda: self.select_mailbox(mailbox=mailbox_list))
  134. select_btn.pack(side="top", pady=2)
  135. def refresh_mailbox(self, mailbox: tk.Listbox):
  136. if self.imap:
  137. mailbox.delete("0", tk.END)
  138. for i in self.imap.list():
  139. mailbox.insert(tk.END, f"{i}")
  140. def login_user(self, host, port, user, passwd, ssl: int, listbox):
  141. try:
  142. port = int(port)
  143. except (TypeError, ValueError):
  144. msg.showerror("Login error", "Port must be int.")
  145. return
  146. if ssl == 1:
  147. ssl = False
  148. start_ssl = False
  149. elif ssl == 2:
  150. ssl = True
  151. start_ssl = False
  152. else:
  153. ssl = False
  154. start_ssl = True
  155. try:
  156. self.imap = Imap(user, passwd, host=host, port=port, ssl=ssl, start_ssl=start_ssl)
  157. self.imap.connect()
  158. self.imap.disconnect()
  159. except imaplib.IMAP4.erro:
  160. msg.showerror("Login error", "Sorry, IMAP Authentication error. Please check your user and password.")
  161. except Exception:
  162. msg.showerror("Login error", "Sorry, Unknown error.")
  163. else:
  164. self.refresh_mailbox(listbox)
  165. msg.showinfo("Login", "Success!")
  166. def select_mailbox(self, mailbox: tk.Listbox):
  167. try:
  168. index = mailbox.curselection()
  169. if len(index) != 1:
  170. msg.showwarning("Select", "Please select a mailbox.")
  171. return
  172. self.imap.inbox = mailbox.get(index[0])
  173. except imaplib.IMAP4.error:
  174. msg.showerror("Select error", "Bad mailbox.")
  175. else:
  176. msg.showinfo("Select", "Success!")
  177. def show_info(self):
  178. if self.imap:
  179. msg.showinfo("Info", f"""Host: {self.imap.host}:{self.imap.port}
  180. User: {self.imap.user}
  181. Passwd: {self.imap.passwd}
  182. SSL: {self.imap.ssl}
  183. MailBox: {self.imap.inbox}""")
  184. else:
  185. msg.showwarning("Not login", "You should login first.")
  186. def show_get(self):
  187. win = tk.Toplevel(self)
  188. win.title("User Login")
  189. win.resizable(False, False)
  190. label_arg = dict(width=GET_LABEL_WIDTH, height=GET_LABEL_HEIGHT)
  191. entry_arg = dict(width=GET_ENTRY_WIDTH)
  192. date_frame = tk.Frame(win)
  193. date_label = tk.Label(date_frame, text="Date: ", **label_arg)
  194. date = tk.Entry(date_frame, **entry_arg)
  195. date_label.pack(side="left")
  196. date.pack(side="left")
  197. date_frame.pack(side="top", pady=2, padx=5)
  198. get_btn = tk.Button(win, text="Get", width=LOGIN_BTN_WIDTH,
  199. command=lambda: self.get_mail(date.get()))
  200. get_btn.pack(side="top", pady=2)
  201. def get_mail(self, date):
  202. if not self.imap:
  203. msg.showwarning("Not login", "You should login first.")
  204. return
  205. if date == "ALL":
  206. pass
  207. elif date == "TODAY":
  208. date = "ON " + strftime('%d-%b-%Y', localtime())
  209. else:
  210. try:
  211. date = "ON " + strftime('%d-%b-%Y', strptime(date, "%Y-%m-%d"))
  212. except ValueError:
  213. msg.showerror("Get error", "Bad date.")
  214. return
  215. if not msg.askyesno("Sure", "Sure to load the mail from the server?\nYou should wait a moment."):
  216. return
  217. try:
  218. self.imap.fetch_all(date)
  219. except imaplib.IMAP4.error:
  220. msg.showerror("Get error", "IMAP4 error, please check setting.")
  221. else:
  222. msg.showinfo("Get", "Success!")
  223. def show_refresh(self):
  224. win = tk.Toplevel(self)
  225. win.title("User Login")
  226. win.resizable(False, False)
  227. label_arg = dict(width=REFRESH_LABEL_WIDTH, height=REFRESH_LABEL_HEIGHT)
  228. entry_arg = dict(width=REFRESH_ENTRY_WIDTH)
  229. start_frame = tk.Frame(win)
  230. start_label = tk.Label(start_frame, text="Start: ", **label_arg)
  231. start = tk.Entry(start_frame, **entry_arg)
  232. start_label.pack(side="left")
  233. start.pack(side="left")
  234. start_frame.pack(side="top", pady=2, padx=5)
  235. step_frame = tk.Frame(win)
  236. step_label = tk.Label(step_frame, text="Step: ", **label_arg)
  237. step = tk.Entry(step_frame, **entry_arg)
  238. step_label.pack(side="left")
  239. step.pack(side="left")
  240. step_frame.pack(side="top", pady=2, padx=5)
  241. get_btn = tk.Button(win, text="Refresh", width=LOGIN_BTN_WIDTH,
  242. command=lambda: self.refresh_mail(start.get(), step.get()))
  243. get_btn.pack(side="top", pady=2)
  244. def refresh_mail(self, start, step):
  245. if not self.imap:
  246. msg.showwarning("Not login", "You should login first.")
  247. return
  248. try:
  249. start = int(start)
  250. step = int(step)
  251. except (TypeError, ValueError):
  252. msg.showerror("refresh error", "Port must be int.")
  253. return
  254. try:
  255. self.mailbox.delete("0", tk.END)
  256. self.mailbox_num = []
  257. mailbox = self.imap.mailbox[start:]
  258. count = 0
  259. for i in mailbox:
  260. self.mailbox.insert(tk.END, f"{i}")
  261. self.mailbox_num.append(i.num)
  262. count += 1
  263. if count == step:
  264. break
  265. except IndexError:
  266. msg.showerror("Refresh error", "Bad Index.")
  267. def show_email(self):
  268. index: tuple = self.mailbox.curselection()
  269. if len(index) != 1:
  270. msg.showwarning("Check", "Please select a email.")
  271. return
  272. else:
  273. index: int = index[0]
  274. if not self.imap:
  275. msg.showwarning("Not login", "You should login first.")
  276. return
  277. email: Mail = self.imap.get_mail(self.mailbox_num[index])
  278. win = tk.Toplevel(self)
  279. win.title("Email info")
  280. win.resizable(False, False)
  281. arg = dict(width=EMAIL_INFO_WIDTH)
  282. from_addr = tk.Label(win, text=f"From: {email.from_addr}", **arg)
  283. title = tk.Label(win, text=f"Title: {email.title}", **arg)
  284. info = st.ScrolledText(win, height=EMAIL_INFO_HEIGHT, state=tk.DISABLED, **arg)
  285. btn_frame = tk.Frame(win)
  286. for i in (from_addr, title, info, btn_frame):
  287. i.pack(side="top", pady=2.5, ipadx=8, padx=2)
  288. button_arg = dict(width=EMAIL_INFO_BUTTON_WIDTH, height=EMAIL_INFO_BUTTON_HEIGHT, border=2)
  289. source = tk.Button(btn_frame, text="Source", command=lambda: self.save_email_source(email), **button_arg)
  290. save = tk.Button(btn_frame, text="Save", command=lambda: self.save_email(email), **button_arg)
  291. file = tk.Button(btn_frame, text="File", command=lambda: self.save_file(email), **button_arg)
  292. for i in (source, save, file):
  293. i.pack(side="left", ipadx=0.5)
  294. self.show_email_text(info, email)
  295. @staticmethod
  296. def show_email_text(text: st.ScrolledText, email: Mail):
  297. text.config(state=tk.NORMAL)
  298. text.delete("0.0", tk.END)
  299. text.insert(tk.END, email.body)
  300. text.config(state=tk.DISABLED)
  301. @staticmethod
  302. def save_email_source(email: Mail):
  303. file = None
  304. try:
  305. file = fd.asksaveasfile(mode="wb", title="Saving email source", filetypes=[("Email", ".email")],
  306. defaultextension=".email")
  307. if file is not None:
  308. file.write(email.byte)
  309. except IOError:
  310. msg.showerror("Save source", "Sorry, IO Error.")
  311. except Exception:
  312. msg.showerror("Save source", "Sorry, Unknown error.")
  313. finally:
  314. if file:
  315. file.close()
  316. msg.showinfo("Saving source", "Success!")
  317. @staticmethod
  318. def save_email(email: Mail):
  319. file = None
  320. try:
  321. file = fd.asksaveasfile(mode="w", title="Saving email text", filetypes=[("Text", ".txt")],
  322. defaultextension=".txt")
  323. if file is not None:
  324. file.write(email.body)
  325. except IOError:
  326. msg.showerror("Save email", "Sorry, IO Error.")
  327. except Exception:
  328. msg.showerror("Save email", "Sorry, Unknown error.")
  329. finally:
  330. if file:
  331. file.close()
  332. msg.showinfo("Saving email", "Success!")
  333. @staticmethod
  334. def save_file(email: Mail):
  335. path = fd.askdirectory(mustexist=True, title="Saving file")
  336. if path is None:
  337. return
  338. try:
  339. email.save_file(path)
  340. except IOError:
  341. msg.showerror("Save file", "Sorry, IO Error.")
  342. except Exception:
  343. msg.showerror("Save file", "Sorry, Unknown error.")
  344. else:
  345. msg.showinfo("Saving file", "Success!")
  346. def save_config(self):
  347. file = None
  348. conf = {}
  349. if self.imap:
  350. conf["imap"] = {}
  351. conf["imap"]["user"] = self.imap.user
  352. conf["imap"]["passwd"] = self.imap.passwd
  353. conf["imap"]["host"] = self.imap.host
  354. conf["imap"]["port"] = self.imap.port
  355. conf["imap"]["ssl"] = self.imap.ssl
  356. conf["imap"]["start_ssl"] = self.imap.start_ssl
  357. conf["imap"]["inbox"] = self.imap.inbox
  358. mailbox = {}
  359. for i in self.imap.mailbox:
  360. byte: bytes = i.byte
  361. mailbox[i.num] = byte.decode("utf-8")
  362. conf["imap"]["mailbox"] = mailbox
  363. try:
  364. file = fd.asksaveasfile(mode="w", title="Saving config file", filetypes=[("JSON", ".json")],
  365. defaultextension=".json")
  366. if file is not None:
  367. file.write(json.dumps(conf))
  368. except IOError:
  369. msg.showerror("Load config", "Sorry, IO Error.")
  370. except Exception:
  371. msg.showerror("Load config", "Sorry, Unknown error.")
  372. finally:
  373. if file:
  374. file.close()
  375. msg.showinfo("Saving success", "Success!")
  376. def load_config(self):
  377. file = None
  378. try:
  379. file = fd.askopenfile(mode="r", title="Loading config file", filetypes=[("JSON", ".json")])
  380. if file is None:
  381. return
  382. conf = json.load(file)
  383. imap = conf.get("imap")
  384. if imap:
  385. self.imap = Imap(
  386. user=imap["user"],
  387. passwd=imap["passwd"],
  388. host=imap.get("host", "localhost"),
  389. port=imap.get("port", 465),
  390. ssl=imap.get("ssl", True),
  391. start_ssl=imap.get("start_ssl", False)
  392. )
  393. self.imap.connect()
  394. self.imap.disconnect()
  395. self.imap.inbox = imap.get("inbox", "INBOX")
  396. mailbox = imap.get("mailbox", {})
  397. for num in imap.get("mailbox", {}):
  398. byte: str = mailbox[num]
  399. self.imap.add_mail(num, byte.encode("utf-8"))
  400. except imaplib.IMAP4.error:
  401. msg.showerror("Load config", "Sorry, IMAP Authentication error. Please check your user and password.")
  402. except KeyError:
  403. msg.showerror("Load config", "Sorry, Key Error.")
  404. except IOError:
  405. msg.showerror("Load config", "Sorry, IO Error.")
  406. except Exception:
  407. msg.showerror("Load config", "Sorry, Unknown error.")
  408. finally:
  409. if file:
  410. file.close()
  411. msg.showinfo("Loading success", "Success!")
  412. if __name__ == '__main__':
  413. window = MailboxGUI()
  414. window.mainloop()