├── EmailReader.py
├── EmailSender.py
├── EmailSenderGUI.py
├── EmailSenderGUI.spec
├── README.md
├── email.ico
├── generate_icon.py
├── icon.py
├── img2base64.py
├── read_file.py
├── requirements.txt
└── temp.html
/EmailReader.py:
--------------------------------------------------------------------------------
1 | import poplib
2 | from email.parser import Parser
3 | import re
4 |
5 |
6 | #退件邮件收件人获取
7 | #连接邮箱服务器
8 | server = poplib.POP3('smtp.xxxx.com')
9 | server.user('xxxx')
10 | server.pass_('xxxx')
11 |
12 | #获取邮件列表
13 | resp, mails, octets = server.list()
14 |
15 | #指定要读取的邮件主题
16 | subject = 'Undelivered Mail Returned to Sender'
17 |
18 | #获取邮件的字符编码,首先在message中寻找编码,如果没有,就在header的Content-Type中寻找
19 | def guess_charset(msg):
20 | charset = msg.get_charset()
21 | if charset is None:
22 | content_type = msg.get('Content-Type', '').lower()
23 | pos = content_type.find('charset=')
24 | if pos >= 0:
25 | charset = content_type[pos+8:].strip()
26 | return charset
27 |
28 | def get_content(msg):
29 | for part in msg.walk():
30 | content_type = part.get_content_type()
31 | charset = guess_charset(part)
32 | #如果有附件,则直接跳过
33 | if part.get_filename()!=None:
34 | continue
35 | email_content_type = ''
36 | content = ''
37 | if content_type == 'text/plain':
38 | email_content_type = 'text'
39 | elif content_type == 'text/html':
40 | continue #不要html格式的邮件
41 | if charset:
42 | try:
43 | content = part.get_payload(decode=True).decode(charset)
44 | #这里遇到了几种由广告等不满足需求的邮件遇到的错误,直接跳过了
45 | except AttributeError:
46 | print('type error')
47 | except LookupError:
48 | print("unknown encoding: utf-8")
49 | if email_content_type =='':
50 | continue
51 | #如果内容为空,也跳过
52 | return content
53 | #邮件的正文内容就在content中
54 |
55 |
56 | if __name__ == '__main__':
57 | f = open("reject.txt", 'a+')
58 | #遍历邮件列表
59 | for i in range(len(mails), 0, -1):
60 | resp, lines, octets = server.retr(i)
61 | #合并邮件内容
62 | message = b'\n'.join(lines).decode('utf-8')
63 | # 将邮件内容解析成Message对象
64 | emailcontent = Parser().parsestr(message)
65 | #如果主题匹配,则输出邮件内容
66 | if subject == emailcontent['Subject'] :
67 | content = get_content(emailcontent)
68 | match = re.findall(r'<.*?>', content)
69 | result = match[0].lstrip('<').rstrip('>')
70 | print(result)
71 | f.write(result)
72 | f.write("\n")
73 | # break
74 | f.close()
75 |
76 |
77 | #关闭连接
78 | server.quit()
--------------------------------------------------------------------------------
/EmailSender.py:
--------------------------------------------------------------------------------
1 | import smtplib
2 | import email
3 | from email.mime.application import MIMEApplication
4 | from email.mime.multipart import MIMEMultipart
5 | from email.mime.text import MIMEText
6 | from email.header import Header
7 | from email.utils import formataddr
8 | import time
9 | import os
10 | from datetime import datetime
11 |
12 | cur_path = os.path.dirname(os.path.realpath(__file__)) # 当前项目路径
13 | # cur_path = os.path.dirname(sys.executable) # 打包运行路径
14 | log_path = cur_path + '\\logs' # log_path为存放日志的路径
15 | if not os.path.exists(log_path): os.mkdir(log_path) # 若不存在logs文件夹,则自动创建
16 |
17 |
18 | def File_Read(file_path):
19 | global Lines
20 | Lines = []
21 | with open(file_path, 'r') as f:
22 | while True:
23 | line = f.readline() # 逐行读取
24 | if not line: # 到 EOF,返回空字符串,则终止循环
25 | break
26 | File_Data(line, flag=1)
27 | File_Data(line, flag=0)
28 |
29 |
30 | def File_Data(line, flag):
31 | global Lines
32 | if flag == 1:
33 | Lines.append(line)
34 | else:
35 | return Lines
36 |
37 |
38 | class Log:
39 |
40 | def __init__(self):
41 | now_time = datetime.now().strftime('%Y-%m-%d')
42 | self.__all_log_path = os.path.join(log_path, now_time + "-all" + ".log") # 收集所有日志信息文件
43 | self.__error_log_path = os.path.join(log_path, now_time + "-error" + ".log") # 收集错误日志信息文件
44 | self.__send_error_log_path = os.path.join(log_path, now_time + "-send_error" + ".log") # 收集发送失败邮箱信息文件
45 | self.__send_done_log_path = os.path.join(log_path, now_time + "-send_done" + ".log") # 收集发送成功邮箱信息文件
46 |
47 | def SaveAllLog(self, message):
48 | with open(r"{}".format(self.__all_log_path), 'a+') as f:
49 | f.write(message)
50 | f.write("\n")
51 | f.close()
52 |
53 | def SaveErrorLog(self, message):
54 | with open(r"{}".format(self.__error_log_path), 'a+') as f:
55 | f.write(message)
56 | f.write("\n")
57 | f.close()
58 |
59 | def SaveSendErrorLog(self, message):
60 | with open(r"{}".format(self.__send_error_log_path), 'a+') as f:
61 | f.write(message)
62 | f.write("\n")
63 | f.close()
64 |
65 | def SaveSendDoneLog(self, message):
66 | with open(r"{}".format(self.__send_done_log_path), 'a+') as f:
67 | f.write(message)
68 | f.write("\n")
69 | f.close()
70 |
71 | def tips(self, message):
72 | now_time_detail = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
73 | print(now_time_detail + ("\033[34m [TIPS]: {}\033[0m").format(message))
74 | self.SaveAllLog(now_time_detail + (" [TIPS]: {}").format(message))
75 |
76 | def info(self, message):
77 | now_time_detail = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
78 | print(now_time_detail + (" [INFO]: {}").format(message))
79 | self.SaveAllLog(now_time_detail + (" [INFO]: {}").format(message))
80 |
81 | def warning(self, message):
82 | now_time_detail = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
83 | print(now_time_detail + ("\033[33m [WARNING]: {}\033[0m").format(message))
84 | self.SaveAllLog(now_time_detail + (" [WARNING]: {}").format(message))
85 |
86 | def error(self, message):
87 | now_time_detail = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
88 | print(now_time_detail + ("\033[31m [ERROR]: {}\033[0m").format(message))
89 | self.SaveAllLog(now_time_detail + (" [ERROR]: {}").format(message))
90 | self.SaveErrorLog(now_time_detail + (" [ERROR]: {}").format(message))
91 |
92 | def send_error(self, message):
93 | self.SaveSendErrorLog(("{}").format(message))
94 |
95 | def done(self, message):
96 | now_time_detail = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
97 | print(now_time_detail + ("\033[32m [DONE]: {}\033[0m").format(message))
98 | self.SaveAllLog(now_time_detail + (" [DONE]: {}").format(message))
99 |
100 | def send_done(self, message):
101 | self.SaveSendDoneLog(("{}").format(message))
102 |
103 |
104 | Log = Log()
105 | Lines = []
106 |
107 |
108 | class EmailSender:
109 | def __init__(self, Subject, From, Content, smtp_user, smtp_passwd, smtp_server, Server=None, email_list=None,
110 | file=None, sleep=0.5):
111 | self.Subject = Subject
112 | self.From = From
113 | self.Content = Content
114 | self.file = file
115 | self.smtp_user = smtp_user
116 | self.smtp_passwd = smtp_passwd
117 | self.smtp_server = smtp_server
118 | self.email_list = email_list
119 | self.Server = Server
120 | self.sleep = sleep
121 |
122 | def Sender(self):
123 | global Lines, client
124 | if not os.path.exists(r"{}".format(self.email_list)):
125 | Log.error('收件人列表文件未找到!')
126 | return
127 | File_Read(self.email_list) # 读取全部内容 ,并以列表方式返回
128 |
129 | for line in Lines:
130 | rcptto = []
131 | rcptto.append(line.rstrip("\n"))
132 | self.victim = line.split('@')[0]
133 | # 显示的Cc收信地址
134 | rcptcc = []
135 | # Bcc收信地址,密送人不会显示在邮件上,但可以收到邮件
136 | rcptbcc = []
137 | # 全部收信地址,包含抄送地址,单次发送不能超过60人
138 | receivers = rcptto + rcptcc + rcptbcc
139 |
140 | # 参数判断
141 | if self.Subject == None or self.Subject == "":
142 | Log.error('邮件主题不存在!')
143 | return
144 | if self.smtp_user == None or self.smtp_user == "":
145 | Log.error('SMTP账号不存在!')
146 | return
147 | if self.smtp_passwd == None or self.smtp_passwd == "":
148 | Log.error('SMTP密码不存在!')
149 | return
150 |
151 | # 构建alternative结构
152 | msg = MIMEMultipart('alternative')
153 | msg['Subject'] = Header(self.Subject)
154 | if self.From[0] == None or self.From[0] == '':
155 | Log.error('发件人名称不存在!')
156 | return
157 | if self.From[1] == None or self.From[1] == '':
158 | self.From[1] = self.smtp_user
159 | msg['From'] = formataddr(self.From) # 昵称+发信地址(或代发)
160 | # list转为字符串
161 | msg['To'] = ",".join(rcptto)
162 | msg['Cc'] = ",".join(rcptcc)
163 | # 自定义的回信地址,与控制台设置的无关。邮件推送发信地址不收信,收信人回信时会自动跳转到设置好的回信地址。
164 | # msg['Reply-to'] = replyto
165 | msg['Message-id'] = email.utils.make_msgid()
166 | msg['Date'] = email.utils.formatdate()
167 |
168 | email_content = self.Content.replace("victim", "victim=" + self.victim)
169 |
170 | # 加载远程图片以标记打开邮件的受害者
171 | if self.Server != ':':
172 | # Log.tips("监听地址: {}".format(self.Server))
173 | listen_code = '
'
174 | else:
175 | listen_code = ''
176 |
177 | # 构建alternative的text/html部分
178 | texthtml = MIMEText(
179 | email_content + listen_code,
180 | _subtype='html', _charset='UTF-8')
181 | msg.attach(texthtml)
182 |
183 | # 附件
184 | if self.file != None and self.file != "":
185 | files = [r'{}'.format(self.file)]
186 | for t in files:
187 | part_attach1 = MIMEApplication(open(t, 'rb').read()) # 打开附件
188 | part_attach1.add_header('Content-Disposition', 'attachment', filename=t.rsplit('/', 1)[1]) # 为附件命名
189 | msg.attach(part_attach1) # 添加附件
190 |
191 | # 发送邮件
192 | try:
193 | client = smtplib.SMTP(self.smtp_server, 25)
194 | client.login(self.smtp_user, self.smtp_passwd)
195 | client.sendmail(self.smtp_user, receivers, msg.as_string()) # 支持多个收件人,最多60个
196 | client.quit()
197 | Log.done("{:<30}\t邮件发送成功!".format(rcptto[0]))
198 | Log.send_done("{}".format(rcptto[0]))
199 | time.sleep(self.sleep)
200 | except Exception as e:
201 | Log.error('邮件发送失败, {}'.format(e))
202 | Log.send_error(rcptto[0])
203 |
204 |
205 | if __name__ == '__main__':
206 | mail_text_path = "./email/email.html" #邮件正文
207 | mail_to_list = "./target/email.txt" #收件人列表
208 | Log.tips("执行邮件正文读取操作!")
209 | f = open(mail_text_path, "r", encoding='utf-8')
210 | Log.done("邮件正文读取完成!")
211 | body_text = f.read()
212 | Log.tips("执行邮件发送操作!")
213 | E = EmailSender("【重要】数据迁移的通知", ['信息安全部', 'admin@admin.com'], body_text, "admin@admin.com", "pass",
214 | "smtp.admin.com", "html.admin.com:8080", mail_to_list)
215 | E.Sender()
216 | Log.done("邮件发送完成!")
217 |
--------------------------------------------------------------------------------
/EmailSenderGUI.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import smtplib
3 | from tkinter.ttk import Combobox
4 |
5 | import email
6 | from email.mime.application import MIMEApplication
7 | from email.mime.multipart import MIMEMultipart
8 | from email.mime.text import MIMEText
9 | from email.header import Header
10 | from email.utils import formataddr
11 | from email.parser import Parser
12 | import time
13 | from tkinter import *
14 | import tkinter.filedialog
15 | import os
16 | from datetime import datetime
17 | import sys
18 | import icon
19 | import re
20 |
21 | class StdoutRedirector(object):
22 | # 重定向输出类
23 | def __init__(self, text_widget):
24 | self.text_space = text_widget
25 | # 将其备份
26 | self.stdoutbak = sys.stdout
27 | self.stderrbak = sys.stderr
28 |
29 | def write(self, str):
30 | self.text_space.tag_config('fc_info', foreground='white')
31 | self.text_space.tag_config('fc_error', foreground='red')
32 | self.text_space.tag_config('fc_done', foreground='green')
33 | self.text_space.tag_config('fc_tips', foreground='cyan')
34 | if 'INFO' in str:
35 | self.text_space.insert(END, str, 'fc_info')
36 | self.text_space.insert(END, '\n')
37 | elif 'ERROR' in str:
38 | self.text_space.insert(END, str, 'fc_error')
39 | self.text_space.insert(END, '\n')
40 | elif 'DONE' in str:
41 | self.text_space.insert(END, str, 'fc_done')
42 | self.text_space.insert(END, '\n')
43 | elif 'TIPS' in str:
44 | self.text_space.insert(END, str, 'fc_tips')
45 | self.text_space.insert(END, '\n')
46 | self.text_space.see(END)
47 | self.text_space.update()
48 |
49 | def restoreStd(self):
50 | # 恢复标准输出
51 | sys.stdout = self.stdoutbak
52 | sys.stderr = self.stderrbak
53 |
54 | def flush(self):
55 | # 关闭程序时会调用flush刷新缓冲区,没有该函数关闭时会报错
56 | pass
57 |
58 |
59 | # cur_path = os.path.dirname(os.path.realpath(__file__)) # 当前项目路径
60 | cur_path = os.path.dirname(sys.executable) # 打包运行路径
61 | log_path = cur_path + '\\logs' # log_path为存放日志的路径
62 | if not os.path.exists(log_path): os.mkdir(log_path) # 若不存在logs文件夹,则自动创建
63 |
64 |
65 | class Log:
66 |
67 | def __init__(self):
68 | now_time = datetime.now().strftime('%Y-%m-%d')
69 | self.__all_log_path = os.path.join(log_path, now_time + "-all" + ".log") # 收集所有日志信息文件
70 | self.__error_log_path = os.path.join(log_path, now_time + "-error" + ".log") # 收集错误日志信息文件
71 | self.__send_error_log_path = os.path.join(log_path, now_time + "-send_error" + ".log") # 收集发送失败邮箱信息文件
72 | self.__send_done_log_path = os.path.join(log_path, now_time + "-send_done" + ".log") # 收集发送成功邮箱信息文件
73 |
74 | def SaveAllLog(self, message):
75 | with open(r"{}".format(self.__all_log_path), 'a+') as f:
76 | f.write(message)
77 | f.write("\n")
78 | f.close()
79 |
80 | def SaveErrorLog(self, message):
81 | with open(r"{}".format(self.__error_log_path), 'a+') as f:
82 | f.write(message)
83 | f.write("\n")
84 | f.close()
85 |
86 | def SaveSendErrorLog(self, message):
87 | with open(r"{}".format(self.__send_error_log_path), 'a+') as f:
88 | f.write(message)
89 | f.write("\n")
90 | f.close()
91 |
92 | def SaveSendDoneLog(self, message):
93 | with open(r"{}".format(self.__send_done_log_path), 'a+') as f:
94 | f.write(message)
95 | f.write("\n")
96 | f.close()
97 |
98 | def tips(self, message):
99 | now_time_detail = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
100 | print(now_time_detail + ("[TIPS]: {}").format(message))
101 | self.SaveAllLog(now_time_detail + (" [TIPS]: {}").format(message))
102 |
103 | def info(self, message):
104 | now_time_detail = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
105 | print(now_time_detail + ("[INFO]: {}").format(message))
106 | self.SaveAllLog(now_time_detail + (" [INFO]: {}").format(message))
107 |
108 | def warning(self, message):
109 | now_time_detail = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
110 | print(now_time_detail + ("[WARNING]: {}").format(message))
111 | self.SaveAllLog(now_time_detail + (" [WARNING]: {}").format(message))
112 |
113 | def error(self, message):
114 | now_time_detail = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
115 | print(now_time_detail + ("[ERROR]: {}").format(message))
116 | self.SaveAllLog(now_time_detail + (" [ERROR]: {}").format(message))
117 | self.SaveErrorLog(now_time_detail + (" [ERROR]: {}").format(message))
118 |
119 | def done(self, message):
120 | now_time_detail = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
121 | print(now_time_detail + ("[DONE]: {}").format(message))
122 | self.SaveAllLog(now_time_detail + (" [DONE]: {}").format(message))
123 |
124 | def send_done(self, message):
125 | self.SaveSendDoneLog(("{}").format(message))
126 |
127 | def send_error(self, message):
128 | self.SaveSendErrorLog(("{}").format(message))
129 |
130 | Log = Log()
131 | stop_flag = 0
132 | start_flag = 0
133 | init_flag = 0
134 | send_index = 0
135 |
136 |
137 | def StopSend():
138 | global stop_flag, start_flag
139 | stop_flag = 1
140 | start_flag = 0
141 |
142 |
143 | def StartSend():
144 | global stop_flag, start_flag
145 | stop_flag = 0
146 | start_flag = 1
147 |
148 |
149 | class EmailSender:
150 | def __init__(self, Subject, From, Content, smtp_user, smtp_passwd, smtp_server, Server=None, email_list=None,
151 | test_user=None,
152 | file=None, img=None, sleep=0.5):
153 | self.Subject = Subject
154 | self.From = From
155 | self.Content = Content
156 | self.file = file
157 | self.smtp_user = smtp_user
158 | self.smtp_passwd = smtp_passwd
159 | self.smtp_server = smtp_server
160 | self.email_list = email_list
161 | self.Server = Server
162 | self.test_user = test_user
163 | self.img = img
164 | self.sleep = sleep
165 |
166 | def Sender(self):
167 | global client
168 | global stop_flag, start_flag
169 | global send_index
170 | if self.test_user != None and self.test_user != '':
171 | lines = [self.test_user]
172 | else:
173 | if not os.path.exists(r"{}".format(self.email_list)):
174 | Log.error('收件人列表文件未找到!')
175 | return False
176 | f = open(r"{}".format(self.email_list), "r")
177 | lines = f.readlines() # 读取全部内容 ,并以列表方式返回
178 |
179 | for line in lines[send_index:]:
180 | if stop_flag == 1:
181 | Log.tips("暂停发送,目前成功发送: {} 封".format(send_index))
182 | Log.tips("发送截至目标: {}".format(line))
183 | break
184 | if line == "\n" or line == '':
185 | continue
186 | rcptto = []
187 | rcptto.append(line.rstrip("\n"))
188 | self.victim = line.split('@')[0]
189 | # 显示的Cc收信地址
190 | rcptcc = []
191 | # Bcc收信地址,密送人不会显示在邮件上,但可以收到邮件
192 | rcptbcc = []
193 | # 全部收信地址,包含抄送地址,单次发送不能超过60人
194 | receivers = rcptto + rcptcc + rcptbcc
195 |
196 | # 构建alternative结构
197 | msg = MIMEMultipart('alternative')
198 | msg['Subject'] = Header(self.Subject)
199 | if self.From[1] == None or self.From[1] == '':
200 | self.From[1] = self.smtp_user
201 | msg['From'] = formataddr(self.From) # 昵称+发信地址(或代发)
202 | # list转为字符串
203 | msg['To'] = ",".join(rcptto)
204 | msg['Cc'] = ",".join(rcptcc)
205 | # 自定义的回信地址,与控制台设置的无关。邮件推送发信地址不收信,收信人回信时会自动跳转到设置好的回信地址。
206 | # msg['Reply-to'] = replyto
207 | msg['Message-id'] = email.utils.make_msgid()
208 | msg['Date'] = email.utils.formatdate()
209 |
210 | email_content = self.Content
211 |
212 | # base64加载图片
213 | if self.img != None and self.img != '':
214 | with open(r"{}".format(self.img), "rb") as i:
215 | b64str = base64.b64encode(i.read())
216 | # Log.info(str(b64str))
217 | img_code = '
'
218 | else:
219 | img_code = ''
220 |
221 | # 加载远程图片以标记打开邮件的受害者
222 | if self.Server != ':':
223 | Log.tips("监听地址: {}".format(self.Server))
224 | listen_code = '
'
225 | else:
226 | listen_code = ''
227 |
228 | # 构建alternative的text/html部分
229 | texthtml = MIMEText(
230 | email_content + img_code + listen_code,
231 | _subtype='html', _charset='UTF-8')
232 | msg.attach(texthtml)
233 |
234 | # 附件
235 | if self.file != None and self.file != "":
236 | files = [r'{}'.format(self.file)]
237 | for t in files:
238 | part_attach1 = MIMEApplication(open(t, 'rb').read()) # 打开附件
239 | # print(t.rsplit('/', 1)[1])
240 | part_attach1.add_header('Content-Disposition', 'attachment', filename=t.rsplit('/', 1)[1]) # 为附件命名
241 | msg.attach(part_attach1) # 添加附件
242 |
243 | # 发送邮件
244 | try:
245 | # 判断长度,当测试单次发送时值为“腾讯企业邮:*25”长度为9,当批量发送时,经过strip('*:25'),长度为5
246 | if len(self.smtp_server) == 9 or len(self.smtp_server) == 5:
247 | self.smtp_server = self.smtp_server.strip('*:25')
248 | if self.smtp_server == "腾讯企业邮":
249 | # 若需要加密使用SSL,可以这样创建client
250 | client = smtplib.SMTP_SSL('smtp.exmail.qq.com', 465)
251 | elif self.smtp_server == "网易企业邮":
252 | client = smtplib.SMTP_SSL('smtphz.qiye.163.com', 465)
253 | elif self.smtp_server == "阿里企业邮":
254 | client = smtplib.SMTP_SSL('smtpdm.aliyun.com', 465)
255 | else:
256 | tmp = self.smtp_server.split('*')[1]
257 | server_ip = tmp.split(':')[0]
258 | server_port = tmp.split(':')[1]
259 | if int(server_port) == 465:
260 | client = smtplib.SMTP_SSL(server_ip, int(server_port))
261 | else:
262 | client = smtplib.SMTP(server_ip, int(server_port))
263 | # 开启DEBUG模式
264 | # client.set_debuglevel(0)
265 | # 发件人和认证地址必须一致
266 | client.login(self.smtp_user, self.smtp_passwd)
267 | # 备注:若想取到DATA命令返回值,可参考smtplib的sendmail封装方法:
268 | # 使用SMTP.mail/SMTP.rcpt/SMTP.data方法
269 | # Log.tips("执行发送")
270 | client.sendmail(self.smtp_user, receivers, msg.as_string()) # 支持多个收件人,最多60个
271 | client.quit()
272 | Log.done('{:<30}\t邮件发送成功!'.format(rcptto[0]))
273 | Log.send_done("{}".format(rcptto[0]))
274 | send_index += 1
275 | time.sleep(self.sleep)
276 | except smtplib.SMTPConnectError as e:
277 | Log.error('邮件发送失败,连接失败: [Code]{},[Error]{}'.format(e.smtp_code, e.smtp_error))
278 | Log.send_error(rcptto[0])
279 | except smtplib.SMTPAuthenticationError as e:
280 | Log.error('邮件发送失败,认证错误: [Code]{},[Error]{}'.format(e.smtp_code, e.smtp_error))
281 | Log.send_error(rcptto[0])
282 | except smtplib.SMTPSenderRefused as e:
283 | Log.error('邮件发送失败,发件人被拒绝: [Code]{},[Error]{}'.format(e.smtp_code, e.smtp_error))
284 | Log.send_error(rcptto[0])
285 | except smtplib.SMTPRecipientsRefused as e:
286 | Log.error('邮件发送失败,收件人被拒绝: [Error]{}'.format(e))
287 | Log.send_error(rcptto[0])
288 | except smtplib.SMTPDataError as e:
289 | Log.error('邮件发送失败,数据接收拒绝:[Code]{},[Error]{}'.format(e.smtp_code, e.smtp_error))
290 | Log.send_error(rcptto[0])
291 | except smtplib.SMTPException as e:
292 | Log.error('邮件发送失败, {}'.format(str(e)))
293 | Log.send_error(rcptto[0])
294 | except Exception as e:
295 | Log.error('邮件发送异常, {}'.format(str(e)))
296 | Log.send_error(rcptto[0])
297 |
298 |
299 | def read_mail(path):
300 | if os.path.exists(path):
301 | with open(path) as fp:
302 | email = fp.read()
303 | return email
304 | else:
305 | Log.error('EML模板文件不存在!')
306 | return None
307 |
308 |
309 | def emailInfo(emailpath):
310 | raw_email = read_mail(emailpath) # 将邮件读到一个字符串里面
311 | if raw_email == None:
312 | return None
313 | # print('EmailPath : ', emailpath)
314 | emailcontent = Parser().parsestr(raw_email) # 经过parsestr处理过后生成一个字典
315 | # for k, v in emailcontent.items():
316 | # print(k, v)
317 | From = emailcontent['From']
318 | To = emailcontent['To']
319 | Subject = emailcontent['Subject']
320 | Date = emailcontent['Date']
321 | MessageID = emailcontent['Message-ID']
322 | XOriginatingIP = emailcontent['X-Originating-IP']
323 |
324 | User = From.split("<")[0].strip()
325 | if "<" in From:
326 | From = re.findall(".*<(.*)>.*", From)[0]
327 | if "<" in To:
328 | To = re.findall(".*<(.*)>.*", To)[0]
329 | if "<" in MessageID:
330 | MessageID = re.findall(".*<(.*)>.*", MessageID)[0]
331 | try:
332 | Subject = base64.b64decode(Subject.split("?")[3]).decode('utf-8')
333 | except Exception:
334 | pass
335 | try:
336 | User = base64.b64decode(User.split("?")[3]).decode('utf-8')
337 | except Exception:
338 | pass
339 |
340 | Log.info("[From-User]:\t{}".format(User))
341 | Log.info("[From]:\t{}".format(From))
342 | Log.info("[X-Originating-IP]:\t{}".format(XOriginatingIP))
343 | Log.info("[To]:\t{}".format(To))
344 | Log.info("[Subject]:\t{}".format(Subject))
345 | Log.info("[Message-ID]:\t{}".format(MessageID))
346 | Log.info("[Date]:\t{}".format(Date))
347 |
348 | # 循环信件中的每一个mime的数据块
349 | for par in emailcontent.walk():
350 | if not par.is_multipart(): # 这里要判断是否是multipart(无用数据)
351 | content = par.get_payload(decode=True)
352 | try:
353 | if content.decode('utf-8', 'ignore').startswith('<'):
354 | # print("content:\t\n", content.decode('utf-8', 'ignore').strip()) # 解码出文本内容,直接输出来就可以了。
355 | return content.decode('utf-8', 'ignore').strip()
356 | except UnicodeDecodeError:
357 | if content.decode('gbk', 'ignore').startswith('<'):
358 | # print("content:\t\n", content.decode('utf-8', 'ignore').strip()) # 解码出文本内容,直接输出来就可以了。
359 | return content.decode('gbk', 'ignore').strip()
360 |
361 |
362 | class EmailSenderGUI():
363 | def __init__(self, init_window):
364 | self.init_window = init_window
365 |
366 | def set_init_windows(self):
367 | self.init_window.title("钓鱼邮件发送工具")
368 | # self.init_window.geometry('1230x810') # 2k窗口大小,若1k分辨率,请自己调试
369 | self.init_window.geometry('1400x1000') # 4k窗口大小
370 | with open('tmp.ico', 'wb') as tmp:
371 | tmp.write(base64.b64decode(icon.Icon().img))
372 | root.iconbitmap('tmp.ico')
373 | os.remove('tmp.ico')
374 | self.init_window.resizable(0, 0)
375 | # pop = Pmw.Balloon(self.init_window)
376 | # 基础配置项
377 | self.basic_setting_label = Label(self.init_window, text="基础配置"
378 | )
379 | self.basic_setting_label.grid(row=0, column=0)
380 | self.basic_setting = Frame(self.init_window)
381 | # SMTP服务器选择
382 | self.smtp_server_label = Label(self.basic_setting, text='SMTP服务器:')
383 | self.smtp_server_label.grid(row=0, column=0)
384 | self.smtp_server_combobox = Combobox(self.basic_setting, width=25)
385 | self.smtp_server_combobox.grid(row=0, column=1, sticky='w', pady=3)
386 | self.smtp_server_combobox['value'] = ('腾讯企业邮', '网易企业邮', '阿里企业邮')
387 | self.smtp_server_combobox.current(0)
388 | self.send_sleep_time_label = Label(self.basic_setting, text="发送间隔(s):")
389 | self.send_sleep_time_label.grid(row=0, column=1, sticky='e', padx=48)
390 | self.send_sleep_time_entry = Entry(self.basic_setting, width=6)
391 | self.send_sleep_time_entry.insert(tkinter.INSERT, '0.5')
392 | self.send_sleep_time_entry.grid(row=0, column=1, sticky='e', pady=3)
393 | # 账号输入框
394 | self.smtp_account_label = Label(self.basic_setting, text="SMTP账号:")
395 | self.smtp_account_label.grid(row=1, column=0)
396 | self.smtp_account_entry = Entry(self.basic_setting, width=45)
397 | # pop.bind(self.smtp_account_label, "必填")
398 | self.smtp_account_entry.grid(row=1, column=1, pady=3)
399 | # 密码输入框
400 | self.smtp_passwd_label = Label(self.basic_setting, text="SMTP密码:")
401 | self.smtp_passwd_label.grid(row=2, column=0)
402 | self.smtp_passwd_entry = Entry(self.basic_setting, show='*', width=45)
403 | # pop.bind(self.smtp_passwd_label, "必填")
404 | self.smtp_passwd_entry.grid(row=2, column=1, pady=3)
405 | # 自定义邮件服务器
406 | self.smtp_server_self_label = Label(self.basic_setting, text="自建SMTP:")
407 | self.smtp_server_self_label.grid(row=3, column=0)
408 | self.smtp_server_self_entry = Entry(self.basic_setting, width=30)
409 | # pop.bind(self.smtp_server_self_entry, "当设置自定邮服时,SMTP选择无效,填写格式:127.0.0.1:465")
410 | self.smtp_server_self_entry.grid(row=3, column=1, sticky='w')
411 | self.smtp_server_self_port_entry = Entry(self.basic_setting, width=8)
412 | self.smtp_server_self_port_entry.insert(tkinter.INSERT, '25')
413 | self.smtp_server_self_blank = Label(self.basic_setting, text="PORT: ", width=5)
414 | self.smtp_server_self_blank.grid(row=3, column=1, sticky='e', padx=60, pady=3)
415 | self.smtp_server_self_port_entry.grid(row=3, column=1, sticky='e', pady=3)
416 | # 监听服务器地址
417 | self.listen_server_label = Label(self.basic_setting, text="监听服务器地址:")
418 | self.listen_server_label.grid(row=4, column=0)
419 | self.listen_server_entry = Entry(self.basic_setting, width=30)
420 | # pop.bind(self.listen_server_entry, "填写格式:127.0.0.1:8080")
421 | self.listen_server_entry.grid(row=4, column=1, sticky='w', pady=3)
422 | self.listen_server_blank = Label(self.basic_setting, text="PORT: ", width=5)
423 | self.listen_server_blank.grid(row=4, column=1, sticky='e', padx=60)
424 | self.listen_server_port_entry = Entry(self.basic_setting, width=8)
425 | self.listen_server_port_entry.grid(row=4, column=1, sticky='e', pady=3)
426 |
427 | # 测试发送
428 | self.test_user_label = Label(self.basic_setting, text="测试接收邮箱:")
429 | self.test_user_label.grid(row=5, column=0)
430 | self.test_user_entry = Entry(self.basic_setting, width=45)
431 | self.test_user_entry.grid(row=5, column=1, pady=3, )
432 | self.test_email_sender = Button(self.basic_setting, text="测试发送", command=lambda: self.SenderHandler(0),
433 | height=1)
434 | self.test_email_sender.grid(row=5, column=2, padx=3)
435 | self.basic_setting.grid(row=1, column=0)
436 |
437 | # 邮件配置
438 | self.email_setting_label = Label(self.init_window, text="邮件配置")
439 | self.email_setting_label.grid(row=2, column=0)
440 | self.email_setting = Frame(self.init_window)
441 | # 邮件头配置
442 | # 伪造发件人配置
443 | self.email_sender_name_label = Label(self.email_setting, text="伪造发件人名称:")
444 | self.email_sender_name_label.grid(row=0, column=0)
445 | self.email_sender_name_entry = Entry(self.email_setting, width=45)
446 | # pop.bind(self.email_sender_name_entry, "必填")
447 | self.email_sender_name_entry.grid(row=0, column=1, pady=3)
448 | self.email_sender_email_label = Label(self.email_setting, text="伪造发件人邮箱:")
449 | self.email_sender_email_label.grid(row=1, column=0)
450 | self.email_sender_email_entry = Entry(self.email_setting, width=45)
451 | self.email_sender_email_entry.grid(row=1, column=1, pady=3)
452 | # 邮件主题
453 | self.email_subject_label = Label(self.email_setting, text="邮件主题:")
454 | self.email_subject_label.grid(row=2, column=0)
455 | self.email_subject_entry = Entry(self.email_setting, width=45)
456 | # pop.bind(self.email_subject_entry, "必填")
457 | self.email_subject_entry.grid(row=2, column=1, pady=3)
458 |
459 | # 邮件原文EML文件选择
460 | self.eml_file_label = Label(self.email_setting, text="EML文件:")
461 | self.eml_file_label.grid(row=3, column=0)
462 | self.EML_file = StringVar()
463 | self.eml_file_entry = Entry(self.email_setting, textvariable=self.EML_file, width=45)
464 | self.eml_file_entry.grid(row=3, column=1, pady=3)
465 | self.eml_file_selector = Button(self.email_setting, text="选择文件",
466 | command=lambda: self.FileSelector(self.eml_file_entry, self.EML_file, 1))
467 | self.eml_file_selector.grid(row=3, column=2, padx=5)
468 |
469 | # 收件人列表
470 | self.email_label = Label(self.email_setting, text="收件人列表:")
471 | self.email_label.grid(row=4, column=0)
472 | self.EMAIL_list = StringVar()
473 | self.email_list_entry = Entry(self.email_setting, textvariable=self.EMAIL_list, width=45)
474 | self.email_list_entry.grid(row=4, column=1, pady=3)
475 | self.email_file_selector = Button(self.email_setting, text="选择文件",
476 | command=lambda: self.FileSelector(self.email_list_entry, self.EMAIL_list, 0))
477 | self.email_file_selector.grid(row=4, column=2, padx=5)
478 |
479 | # 邮件附件选择
480 | self.file_label = Label(self.email_setting, text="邮件附件:")
481 | self.file_label.grid(row=5, column=0)
482 | self.EMAIL_file = StringVar()
483 | self.file_entry = Entry(self.email_setting, textvariable=self.EMAIL_file, width=45)
484 | self.file_entry.grid(row=5, column=1, pady=3)
485 | self.file_selector = Button(self.email_setting, text="选择附件",
486 | command=lambda: self.FileSelector(self.file_entry, self.EMAIL_file, 0))
487 | self.file_selector.grid(row=5, column=2, padx=5)
488 |
489 | # 邮件正文图片插入
490 | self.img_label = Label(self.email_setting, text="正文图片:")
491 | self.img_label.grid(row=6, column=0)
492 | self.EMAIL_img = StringVar()
493 | self.img_entry = Entry(self.email_setting, textvariable=self.EMAIL_img, width=45)
494 | self.img_entry.grid(row=6, column=1, pady=3)
495 | self.img_selector = Button(self.email_setting, text="选择图片",
496 | command=lambda: self.FileSelector(self.img_entry, self.EMAIL_img, 0))
497 | self.img_selector.grid(row=6, column=2, padx=5)
498 | self.email_setting.grid(row=3, column=0)
499 |
500 | # 邮件编辑区头部
501 | self.email_editor_label = Label(self.init_window, text="邮件正文编辑")
502 | self.email_editor_label.grid(row=0, column=1)
503 | # 邮件编辑区
504 | self.email_editor = Frame(self.init_window)
505 | self.email_editor_text = Text(self.email_editor, height=56, width=90)
506 | self.email_editor_text.grid(row=0, column=0)
507 | self.email_editor.grid(row=1, column=1, rowspan=5)
508 | self.email_editor_text.tag_configure("found", background="yellow")
509 |
510 | # 正文搜索
511 | self.email_function_frame = Frame(self.init_window)
512 | self.email_searcher_entry = Entry(self.email_function_frame, width=35)
513 | self.email_searcher_entry.grid(row=0, column=0, pady=3)
514 | self.email_searcher = Button(self.email_function_frame, text="搜索", command=self.TextSearcher)
515 | self.email_searcher.grid(row=0, column=1, padx=5)
516 |
517 | # 邮件按钮
518 | self.email_preview = Button(self.email_function_frame, text="邮件预览", command=self.HTMLRunner)
519 | self.email_preview.grid(row=0, column=2, padx=5)
520 | # 邮件发送
521 | self.email_sender = Button(self.email_function_frame, text="批量发送", command=lambda: self.SenderHandler(1))
522 | self.email_sender.grid(row=0, column=3, padx=5)
523 | # 重置
524 | self.email_reset_sender = Button(self.email_function_frame, text="重置", command=lambda: self.Reset())
525 | self.email_reset_sender.grid(row=0, column=4, padx=5)
526 | self.email_function_frame.grid(column=1)
527 |
528 | # 控制台输出
529 | self.log_print_label = Label(self.init_window, text="控制台输出")
530 | self.log_print_label.grid(row=4, column=0)
531 | self.log_print_from = Frame(self.init_window)
532 | self.log_print_window = Text(self.log_print_from, bg='black')
533 | sys.stdout = StdoutRedirector(self.log_print_window)
534 | self.log_print_window.grid(row=0, column=0)
535 | self.log_print_from.grid(row=5, column=0, padx=10)
536 |
537 | # banner区
538 | self.banner_frame = Frame(self.init_window)
539 | self.banner_label = Label(self.banner_frame, text="Github: https://github.com/A10ha ")
540 | self.banner_label.grid(row=0, column=0)
541 | self.banner_frame.grid(row=6, column=0, pady=20)
542 |
543 | def TextSearcher(self):
544 | self.email_editor_text.tag_remove("found", "1.0", END)
545 | start = "1.0"
546 | key = self.email_searcher_entry.get()
547 |
548 | if (len(key.strip()) == 0):
549 | return
550 | while True:
551 | pos = self.email_editor_text.search(key, start, END)
552 | # print("pos= ",pos) # pos= 3.0 pos= 4.0 pos=
553 | if (pos == ""):
554 | break
555 | self.email_editor_text.tag_add("found", pos, "%s+%dc" % (pos, len(key)))
556 | start = "%s+%dc" % (pos, len(key))
557 |
558 | def FileSelector(self, File_Entry, Entry_Value, flag):
559 | Entry_Value.set('')
560 | self.filename = tkinter.filedialog.askopenfilename()
561 | if self.filename != '':
562 | File_Entry.insert(tkinter.INSERT, self.filename)
563 | if flag == 1:
564 | self.HTMLReader(self.filename)
565 |
566 | def HTMLRunner(self):
567 | HTML_content = self.email_editor_text.get("0.0", "end")
568 | if self.img_entry.get() != None and self.img_entry.get() != '':
569 | with open(r"{}".format(self.img_entry.get()), "rb") as i:
570 | b64str = base64.b64encode(i.read())
571 | img_code = '
'
573 | else:
574 | img_code = ''
575 | # self.email_editor_text.insert(tkinter.INSERT, img_code)
576 | with open('temp.html', 'wb') as f:
577 | HTML = HTML_content + img_code
578 | f.write(HTML.encode('utf-8'))
579 | os.popen('start temp.html')
580 |
581 | def HTMLReader(self, eml_file):
582 | self.email_editor_text.delete('1.0', 'end')
583 | Log.tips("================================================")
584 | HTML_content = emailInfo(eml_file)
585 | self.email_editor_text.insert(tkinter.INSERT, HTML_content)
586 | with open('temp.html', 'wb') as f:
587 | f.write(HTML_content.encode('utf-8'))
588 |
589 | def SenderHandler(self, flag):
590 | global start_flag, stop_flag, init_flag, E
591 | if init_flag == 0:
592 | self.InitSender(flag)
593 | return
594 | if start_flag == 1:
595 | StopSend()
596 | self.email_sender['text'] = "恢复发送"
597 | self.email_reset_sender['state'] = 'normal'
598 | else:
599 | StartSend()
600 | self.email_sender['text'] = "暂停发送"
601 | self.email_reset_sender['state'] = 'disabled'
602 | E.Sender()
603 |
604 | def Reset(self):
605 | global init_flag, start_flag, stop_flag, send_index
606 | init_flag = 0
607 | start_flag = 0
608 | stop_flag = 0
609 | send_index = 0
610 | time.sleep(0.5)
611 | Log.tips("发送队列已重置,可重新选择收件人邮箱文件进行重新发送。\n")
612 | self.email_sender['text'] = "批量发送"
613 | self.test_email_sender['state'] = 'normal'
614 | self.email_editor_text['state'] = 'normal'
615 | self.send_sleep_time_entry['state'] = 'normal'
616 | self.email_sender_name_entry['state'] = 'normal'
617 | self.email_sender_email_entry['state'] = 'normal'
618 | self.email_subject_entry['state'] = 'normal'
619 |
620 | def InitSender(self, flag):
621 | global init_flag, start_flag, send_index, E
622 | if self.smtp_server_self_entry.get() == None or self.smtp_server_self_entry.get() == '':
623 | Log.tips("SMTP服务器: {}".format(self.smtp_server_combobox.get()))
624 | else:
625 | Log.tips(
626 | "SMTP服务器: {}".format(self.smtp_server_self_entry.get() + ':' + self.smtp_server_self_port_entry.get()))
627 | # 参数判断
628 | if self.email_subject_entry.get() == None or self.email_subject_entry.get() == "":
629 | Log.error('邮件主题不存在!')
630 | return False
631 | if self.smtp_account_entry.get() == None or self.smtp_account_entry.get() == "":
632 | Log.error('SMTP账号不存在!')
633 | return False
634 | if self.smtp_passwd_entry.get() == None or self.smtp_passwd_entry.get() == "":
635 | Log.error('SMTP密码不存在!')
636 | return False
637 | if self.email_sender_name_entry.get() == None or self.email_sender_name_entry.get() == '':
638 | Log.error('发件人名称不存在!')
639 | return False
640 | if flag == 0:
641 | if self.test_user_entry.get() == None or self.test_user_entry.get() == '':
642 | Log.error("测试收件人邮箱未设置!")
643 | else:
644 | Log.tips("测试发送: {}".format(self.test_user_entry.get()))
645 | E = EmailSender(self.email_subject_entry.get(),
646 | [self.email_sender_name_entry.get(), self.email_sender_email_entry.get()],
647 | self.email_editor_text.get("0.0", "end"),
648 | self.smtp_account_entry.get(), self.smtp_passwd_entry.get(),
649 | self.smtp_server_combobox.get() + '*' + self.smtp_server_self_entry.get() + ':' + self.smtp_server_self_port_entry.get(),
650 | self.listen_server_entry.get() + ':' + self.listen_server_port_entry.get(),
651 | None,
652 | self.test_user_entry.get(),
653 | self.file_entry.get(), self.img_entry.get(), float(self.send_sleep_time_entry.get()))
654 | send_index = 0
655 | E.Sender()
656 | elif flag == 1:
657 | if self.email_list_entry.get() == None or self.email_list_entry.get() == '':
658 | Log.error("收件人列表文件路径未设置!")
659 | else:
660 | init_flag = 1
661 | start_flag = 1
662 | self.email_sender['text'] = "暂停发送"
663 | self.email_reset_sender['state'] = 'disabled'
664 | self.test_email_sender['state'] = 'disabled'
665 | self.email_editor_text['state'] = 'disabled'
666 | self.send_sleep_time_entry['state'] = 'disabled'
667 | self.email_sender_name_entry['state'] = 'disabled'
668 | self.email_sender_email_entry['state'] = 'disabled'
669 | self.email_subject_entry['state'] = 'disabled'
670 | Log.tips("批量发送: {}".format(self.email_list_entry.get()))
671 | E = EmailSender(self.email_subject_entry.get(),
672 | [self.email_sender_name_entry.get(), self.email_sender_email_entry.get()],
673 | self.email_editor_text.get("0.0", "end"),
674 | self.smtp_account_entry.get(), self.smtp_passwd_entry.get(),
675 | self.smtp_server_combobox.get() + '*' + self.smtp_server_self_entry.get() + ':' + self.smtp_server_self_port_entry.get(),
676 | self.listen_server_entry.get() + ':' + self.listen_server_port_entry.get(),
677 | self.email_list_entry.get(),
678 | None,
679 | self.file_entry.get(), self.img_entry.get(), float(self.send_sleep_time_entry.get()))
680 | E.Sender()
681 |
682 |
683 | if __name__ == '__main__':
684 | root = Tk() # 实例化出一个父窗口
685 | PORTAL = EmailSenderGUI(root)
686 | # 设置根窗口默认属性
687 | PORTAL.set_init_windows()
688 | # 父窗口进入事件循环,可以理解为保持窗口运行,否则界面不展示
689 | root.mainloop()
690 |
--------------------------------------------------------------------------------
/EmailSenderGUI.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 |
3 |
4 | block_cipher = pyi_crypto.PyiBlockCipher(key='password')
5 |
6 |
7 | a = Analysis(['EmailSenderGUI.py'],
8 | pathex=[],
9 | binaries=[],
10 | datas=[],
11 | hiddenimports=[],
12 | hookspath=[],
13 | hooksconfig={},
14 | runtime_hooks=[],
15 | excludes=[],
16 | win_no_prefer_redirects=False,
17 | win_private_assemblies=False,
18 | cipher=block_cipher,
19 | noarchive=False)
20 | pyz = PYZ(a.pure, a.zipped_data,
21 | cipher=block_cipher)
22 |
23 | exe = EXE(pyz,
24 | a.scripts,
25 | a.binaries,
26 | a.zipfiles,
27 | a.datas,
28 | [],
29 | name='EmailSenderGUI',
30 | debug=False,
31 | bootloader_ignore_signals=False,
32 | strip=False,
33 | upx=True,
34 | upx_exclude=[],
35 | runtime_tmpdir=None,
36 | console=False,
37 | disable_windowed_traceback=False,
38 | target_arch=None,
39 | codesign_identity=None,
40 | entitlements_file=None , icon='email.ico')
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # EmailSender
2 | 钓鱼邮件便捷发送工具(GUI)
3 | # **程序简介**
4 |
5 | 本程序利用Python语言编写,使用Tkinter实现图形化界面,可使用Pyinstaller进行exe打包,程序主界面截图如下:
6 | 
7 |
8 | # **功能简介**
9 |
10 | 1. 支持腾讯企业邮、网易企业邮、阿里企业邮、自建邮服SMTP授权账号(其他邮服,可在自建SMTP服务器处填写,默认使用25端口,无SSL)。
11 | 2. 批量发送钓鱼邮件,可暂停发送,可重置发送。
12 | 3. 伪造发件人名称与发件人邮箱。
13 | 4. 解析EML邮件原文文件,快捷利用邮件原文生成钓鱼邮件。
14 | 5. 快捷邮件正文HTML代码编辑预览,快速本地调试邮件内容。
15 | 6. 设置监听服务器,监听接收受害者是否打开邮件,适用于应急演练场景。
16 | 7. 本地日志记录,自动创建本地日志目录,记录全量日志以及错误日志。
17 | 8. 正文图片Base64编码插入,可用于插入钓鱼网页二维码等图片。
18 |
19 | # **关键参数说明**
20 |
21 | 1. SMTP账号密码必填,由相关企业邮设置-客户端授权生成。
22 | 2. 自建SMTP优先级最高,若要使用其他官方SMTP服务器请将自建SMTP置空。
23 | 3. 监听服务器地址选填,可开启HTTP服务监听访问。
24 | 4. EML文件导入,由邮箱导出邮件,选择后,可进行邮件正文(HTML代码)编辑,通过邮件预览查看样式。
25 | 5. 收件人列表,批量发送时配置,使用txt文件指定,每个邮箱使用换行分割。
26 |
27 | # **开发思路**
28 |
29 | # **发送邮件函数**
30 |
31 | 利用**python**的**email**库,使用**smtplib**创建SMTP通信客户端与服务端通信发送邮件。
32 |
33 | 配置发送人信息参数实现发件人信息伪造,有些邮件管理客户端会存在代发字段。
34 |
35 | 利用在正文中添加隐藏图片代码,当受害者打开邮件时,请求攻击者监听服务器资源,达到识别邮件是否启封。
36 |
37 | # **EML文件处理**
38 |
39 | 利用**python**的**email**库,使用**Parser()**进行邮件原文文件解析,之后针对解析出来的信息进行处理输出。
40 |
41 | # **HTML正文编辑预览**
42 |
43 | 利用邮件原文文件解析处理的正文**HTML**信息,定向输出到**Tkinter**的**Text()**控件中,达到即时编辑。
44 |
45 | 通过**Python**的**File**类函数将**Text()**控件中的内容写入本地**temp.html**中利用**os**库执行”**start temp.html**“,利用本地浏览器打开文件,实现邮件预览。
46 |
47 | # 本项目是仅为个人研究练习开发,禁止用于非法活动。
48 | # 项目开发者对软件的滥用不承担任何责任。 由使用者负责。
49 |
--------------------------------------------------------------------------------
/email.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/A10ha/EmailSender/8254c5782ac2295ebdf74b34326b0f30687efd53/email.ico
--------------------------------------------------------------------------------
/generate_icon.py:
--------------------------------------------------------------------------------
1 | import base64
2 | with open("icon.py","a") as f:
3 | f.write('class Icon(object):\n')
4 | f.write('\tdef __init__(self):\n')
5 | f.write("\t\tself.img='")
6 | with open("email.ico","rb") as i:
7 | b64str = base64.b64encode(i.read())
8 | with open("icon.py","ab+") as f:
9 | f.write(b64str)
10 | with open("icon.py","a") as f:
11 | f.write("'")
--------------------------------------------------------------------------------
/icon.py:
--------------------------------------------------------------------------------
1 | class Icon(object):
2 | def __init__(self):
3 | self.img='AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAABTTEgLVEtGfVRLReBUTkr+VE9L/1RPS/9UT0v/VE9L/1RPS/9UT0v/VE9L/1RPS/9UT0v/VE9L/1RPS/9UT0v/VE9L/1RPS/9UT0v/VE9L/1RPS/9UT0v/VE9L/1RPS/9UT0v/VE9L/1RPS/9UT0v/VE5K/VRLRd9US0V8VE1IC1RLRnxTU1D0T4WX/02lw/9Mp8X/TKbE/0ymxP9MpsT/TKbE/0ymxP9MpsT/TKbE/0ymxP9MpsT/TKbE/0ymxP9MpsT/TKbE/0ymxP9MpsT/TKbE/0ymxP9MpsT/TKbE/0ymxP9MpsT/TKbE/0ynxf9NpsP/T4eZ/1NTUfRUS0V7VEtG3E+EmP9Jz/z+SdL//0nS//9J0v//SdL//0nS//9J0v//SdL//0nS//9J0v//SdL//0nS//9J0v//SdL//0nS//9J0v//SdL//0nS//9J0v//SdL//0nS//9J0v//SdL//0nS//9J0v//SdL//0nS//9J0P3/T4WZ/1RLRtxUTkr9Spa//UjK//tJ0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0jK//xKlr/+VE5K/VROS/9Kk7//Rbn6/EjI/PtJ0P3/SdH+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdH//0nQ/v9IyPz8Rbn6/kqTv/9UTkv/VE5L/0qTv/9Ftvn/RrLv/U6Jn/1Mr8//SdH//0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nR//9MsNH/Toyk/ka08P5Ftvn/SpO//1ROS/9UTkv/SpO//0W2+f9GsO//T2+D/1NeX/9Mrs7/SdH//0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0f//TK/Q/1JfYf9PcYX/RrDw/0W2+f9Kk7//VE5L/1ROS/9Kk7//Rbb5/0W19/9Gqub/T2x+/1NeYP9Mrs7/SdH//0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdH//0yv0P9SX2H/T26A/0aq5v9Ftff/Rbb5/0qTv/9UTkv/VE5L/0qTv/9Ftvn/RbT2/0W1+P9Gq+b/T2x+/1NeYP9Mrs7/SdH//0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nR//9Mr9D/Ul9i/09tgP9Gq+b/RbX4/0W09v9Ftvn/SpO//1ROS/9UTkv/SpO//0W2+f9FtPb/RbT2/0W1+P9Gq+b/T2x+/1NeYP9Mrs7/SdH//0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0P7/SdD+/0nQ/v9J0f//TK/Q/1JfYf9PbYD/Rqvm/0W1+P9FtPb/RbT2/0W2+f9Kk7//VE5L/1ROS/9Kk7//Rbb5/0W09v9FtPb/RbT2/0W1+P9Gq+b/T2x+/1NeYP9Mrs7/SdH//0nQ/v9J0P7/SdH+/0nR//9J0f//SdH+/0nQ/v9J0P7/SdH//0yv0P9SX2H/T22A/0ar5v9Ftfj/RbT2/0W09v9FtPb/Rbb5/0qTv/9UTkv/VE5L/0qTv/9Ftvn/RbT2/0W09v9FtPb/RbT2/0W1+P9Gq+b/T2x+/1NeYP9Mrs7/SdH//0nS//9Kxu//TZSs/02UrP9KxvD/SdL//0nS//9Mr9D/Ul9h/09tgP9Gq+b/RbX4/0W09v9FtPb/RbT2/0W09v9Ftvn/SpO//1ROS/9UTkv/SpO//0W2+f9FtPb/RbT2/0W09v9FtPb/RbT2/0W1+P9Gq+b/T2x+/1NeYP9Mrc7/S6/R/1Budv9hWlX/YVpV/09ud/9Lr9H/TK/Q/1JgYv9PboD/Rqvm/0W1+P9FtPb/RbT2/0W09v9FtPb/RbT2/0W2+f9Kk7//VE5L/1ROS/9Kk7//Rbb5/0W09v9FtPb/RbT2/0W09v9FtPb/RbT2/0W1+P9GrOj/UGl4/1JYWP9VWln/gnt2/8/OzP/Pzsz/gXp1/1VZWf9RWFj/UGp5/0as6P9Ftfj/RbT2/0W09v9FtPb/RbT2/0W09v9FtPb/Rbb5/0qTv/9UTkv/VE5L/0qTv/9Ftvn/RbT2/0W09v9FtPb/RbT2/0W09v9Ftff/RbP0/0mXxf9PX2j/Z15Z/6yopP/o6Of/9PTz//T08//o6Ob/rKil/2dfWf9PX2j/SZfF/0Wz9P9Ftff/RbT2/0W09v9FtPb/RbT2/0W09v9Ftvn/SpO//1ROS/9UTkv/SpO//0W2+f9FtPb/RbT2/0W09v9FtPb/RbX4/0ep4/9Me5f/VlZV/4eAfP/S0c//8/Py//Pz8v/z8/L/8/Py//Pz8v/z8/L/09LQ/4aAe/9WVVX/THuX/0ep4/9Ftfj/RbT2/0W09v9FtPb/RbT2/0W2+f9Kk7//VE5L/1ROS/9Kk7//Rbb5/0W09v9FtPb/RbX3/0Wy8/9Jk77/T2Bq/2lhXP+zr6z/6+vq//T08//y8vH/9PTz/9jX1f/Y19X/9PTz//Ly8f/09PP/6+vp/7OvrP9pYVz/T2Br/0mTv/9Fs/T/RbX3/0W09v9FtPb/Rbb5/0qTv/9UTkv/VE5L/0qTv/9Ftvn/RbT3/0W1+P9Hp+D/THWN/1hXVf+MhoH/1tTT//Pz8v/z8/L/8/Py/+/u7f/h4d//jomF/46Jhf/h4N//7+7t//Pz8v/z8/L/8/Py/9bV0/+MhoH/WFdW/0x1jv9Hp+D/RbX4/0W09/9Ftvn/SpO//1ROS/9UTkv/SpO//0W3+/9FsfH/SpC6/1BdZf9uZmL/t7Ow/+zs6//09PP/8vLx//Ly8f/s6+r/m5aT/2dgW/9aU07/WlNO/2dgW/+cl5T/7Ozr//Ly8f/y8vH/9PTz/+zs6/+3s7H/bmZh/1BdZf9Kkbv/RbHx/0W3+/9Kk7//VE5L/1ROS/9KlMH/R6Xd/05zif9aVlT/kYuH/9va2P/09PP/8/Py//Ly8f/y8vH/9PTz/8XDwf9hWVX/n5uY/7Wyr/+1sq//oJuY/2FaVf/Fw8H/9PTz//Ly8f/y8vH/8/Py//T08//b2tj/kYqG/1pWU/9Oc4r/R6be/0qUwP9UTkv/VE5L/09vgv9RXWT/XGVm/7W4t//v7u3/8/Pz//Ly8f/y8vH/8vLx//Ly8f/19fT/uLaz/21mYv/k4+H/9/f3//f39//j4uD/Z2Bb/7Kvrf/19fT/8vLx//Ly8f/y8vH/8vLx//P08//u7u3+g626+1BjZ/9RXWT/T2+D/1ROS/xUTEf+U1RS/06DlP9fxOb/4fH2//Tz8f/y8vH/8vLx//Ly8f/y8vH/8vLx//Pz8v/m5eT/zcvJ/93c2v/f3t3/4ODe/8rIxv9jXFj/u7i2//X19P/y8vH/8vLx//Ly8f/y8vH/8vLx//Pz8v6K4fz6R8Dp/1CCkf9UTkn/VExH2lROSpBOmbK9SM777lnB5P7O2Nr/8/Px//Ly8f/y8vH/8vLx//Ly8f/y8vH/8vLx/+zs6/+dmZX/Z2Bc/2ZfW/9nYFv/Y1xY/395df/i4eD/8/Py//Ly8f/y8vH/8vLx//Ly8f/z8/L/6Ofm/ny0xfxKp8X/T4WW/1RQTPVUS0Z9VEw2AUfi/AtJ0/81T2Vru5eSj//z8/L/8vLx//Ly8f/y8vH/8vLx//Ly8f/09PP/xsPB/2FaVf+fm5j/tLGu/7Owrf+7uLb/2dfW//Dw7//y8vH/8vLx//Ly8f/y8vH/8vLx//T19P/Fw8H/WVNP+1NOSuNUS0bGVEtGbVRNSAkAAAAAWj0uAEVuggBQRkGbkIuI//Ly8f/y8vH/8vLx//Ly8f/y8vH/8vLx//X19P+zsK3/Z2Bb/+Pi4P/39/f/9/f2/+jo5/+GgHz/xsPB//T08//y8vH/8vLx//Ly8f/y8vH/9fX0/8C+u/9XT0rkU0pFOFRLRg1UTUgBVExHAAAAAABYUEgARj5EAFBHQpuRjIj/8vLx//Ly8f/y8vH/8vLx//Ly8f/y8vH/9fX0/7u4tv9jXFf/ysjG/+Dg3v/h4N7/ysjG/2JaVv+6t7X/9fX0//Ly8f/y8vH/8vLx//Ly8f/19fT/wL68/1dQS+FSSkUeVExHAAAAAAAAAAAAAAAAAFhQSABGPkQAUEdCm5GMiP/y8vH/8vLx//Ly8f/y8vH/8vLx//Ly8f/z8/L/4uHf/355df9jXFf/Z2Bb/2dgW/9jXFj/fnh0/+Lh4P/z8/L/8vLx//Ly8f/y8vH/8vLx//X19P/Avrz/V1BL4VJKRR5UTEcAAAAAAAAAAAAAAAAAWFBIAEY+RABQR0KbkYyI//Ly8f/y8vH/8vLx//Ly8f/y8vH/8vLx//Ly8f/z8/L/4uHf/7q4tf91b2v/dm9r/7y5tv/i4eD/8/Py//Ly8f/y8vH/8vLx//Ly8f/y8vH/9fX0/8C+vP9XUEvhUkpFHlRMRwAAAAAAAAAAAAAAAABYUEgARj5DAFBHQpuRjIj/8vLx//Ly8f/y8vH/8vLx//Ly8f/y8vH/8vLx//Ly8f/z8/L/9fb1/8C+vP/Bv73/9fb1//Pz8v/y8vH/8vLx//Ly8f/y8vH/8vLx//Ly8f/19fT/wL68/1dQS+FSSkUeVExHAAAAAAAAAAAAAAAAAFhQSABIQEQAUEdCm5GMiP/19fT/9fX0//X19P/19fT/9fX0//X19P/19fT/9fX0//X19P/19fT/9vb1//b29f/19fT/9fX0//X19P/19fT/9fX0//X19P/19fT/9fX0//f39//Bv73/V09L4VJKRR5UTEcAAAAAAAAAAAAAAAAAWFBIAFRMRwBRSUSGcmtn/7u4tv/Bvrz/wL68/8C+vP/Avrz/wL68/8C+vP/Avrz/wL68/8C+vP/Avrz/wL68/8C+vP/Avrz/wL68/8C+vP/Avrz/wL68/8C+vP/Avrz/wb68/5CLh/9UTEfLU0tGE1RMRwAAAAAAAAAAAAAAAAAAAAAAVExHAFRMRzVTS0bMVk9K/ldQS/9XUEv/V1BL/1dQS/9XUEv/V1BL/1dQS/9XUEv/V1BL/1dQS/9XUEv/V1BL/1dQS/9XUEv/V1BL/1dQS/9XUEv/V1BL/1dQS/9XT0r/VExH6lRMR2hQSEMAVU1IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAeAAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAA8='
--------------------------------------------------------------------------------
/img2base64.py:
--------------------------------------------------------------------------------
1 | import base64
2 |
3 | if __name__ == '__main__':
4 | with open("email.ico", "rb") as i:
5 | b64str = base64.b64encode(i.read())
6 | img_code = '
'
7 | with open("img.html", "wb") as f:
8 | f.write(img_code.encode('utf-8'))
9 |
--------------------------------------------------------------------------------
/read_file.py:
--------------------------------------------------------------------------------
1 | Lines = []
2 |
3 |
4 | def File_Read(file_path):
5 | global Lines
6 | Lines = []
7 | with open(file_path, 'r') as f:
8 | while True:
9 | line = f.readline() # 逐行读取
10 | if not line: # 到 EOF,返回空字符串,则终止循环
11 | break
12 | File_Data(line, flag=1)
13 | File_Data(line, flag=0)
14 |
15 |
16 | def File_Data(line, flag):
17 | global Lines
18 | if flag == 1:
19 | Lines.append(line)
20 | else:
21 | return Lines
22 |
23 |
24 | if __name__ == '__main__':
25 | File_Read('./test_email.txt')
26 | print(Lines)
27 | File_Read('./email.txt')
28 | print(Lines)
29 | # print(line)
30 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/A10ha/EmailSender/8254c5782ac2295ebdf74b34326b0f30687efd53/requirements.txt
--------------------------------------------------------------------------------
/temp.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
492 |
493 |
494 |
496 |
It's hard to say goodbye.
497 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 |
507 |
508 | View e-mail in browser
509 | |
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 |
518 |
519 | |
520 |
521 |
522 | The ReadME Project is a GitHub platform dedicated to highlighting the best from the open source software community—the people and tech behind projects you use every day. This special edition newsletter includes a Q&A with industry experts covering topics like teaching, technical interviewing, and more.
523 | |
524 |
525 |
526 |
527 |
528 |
529 | Community Q&A
530 | |
531 |
532 |
533 | |
534 |
535 |
536 | Gracefully sunsetting open source projects
537 | There are lots of reasons to sunset an open source project. Maybe you don't have time to maintain it and want to ensure users with dependencies have time to transition. Or maybe the problem the project was meant to address no longer exists. Or perhaps your project is no longer compatible with modern technologies.
538 |
539 | Whatever your reasons, you want to end the project gracefully. To help you learn the dos and don'ts of project deprecation, The ReadME Project senior editor Klint Finley gathered a panel of three open source maintainers with experience sunsetting projects.
540 |
541 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 |
554 |
555 | |
556 |
557 |
558 | Ben Johnson
559 | Ben Johnson is an open source software developer specializing in writing databases and distributed systems. He's the maintainer of the SQLite recovery tool Litestream, a former maintainer of the now deprecated Go language database BoltDB, and a software engineer at Fly.io.
560 | |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 | |
572 |
573 |
574 | Olga Botvinnik
575 | Olga Botvinnik is a computational biologist and the creator of the Python data visualization package prettyplotlib and a contributor to the Seaborn visualization package. She's currently a statistical geneticist at Bridge Bio, where she uses functional genomics to prioritize targets for rare genetic diseases.
576 | |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 | |
588 |
589 |
590 | Brett Terpstra
591 | Brett Terpstra is a software developer and writer at Oracle. He's been publishing open source utilities for Mac and Unix users for 20 years and maintains over 100 GitHub repositories, including the note-taking app nvALT, logging script Slogger, and productivity app Doing. He plans to retire nvALT in favor of a commercial version.
592 | |
593 |
594 |
595 | |
596 |
597 |
598 |
599 | |
600 |
601 |
602 | Klint: How did you know when it was time to sunset a project, as opposed to handing it off to new maintainers?
603 |
604 | Ben: By the time I decided to sunset BoltDB, the CoreOS team had already created a fork called BBolt. I didn't want to just hand the project over to someone else and the fact that a fork existed made me more comfortable saying I wasn't going to update Bolt anymore. My name and reputation were pretty closely tied to the project at the time. I was the BoltDB guy. I didn’t want to put my reputation in the hands of someone else. It made more sense to me to make a clean break and point people at a fork.
605 |
606 | Olga: First of all, maintaining a project takes a lot of work. When I sunset prettyplotlib, I was in graduate school, dealing with research, publishing, and everything else that was going on with my life. I didn't feel like I had time to keep pushing prettyplotlib forward. I was still using Python 2.7 because I had used it throughout grad school. I wasn't sure how much work it would be to migrate prettyplotlib to Python 3. It was a big unknown. I was feeling guilty about stopping work on prettyplotlib since so many people were using it, but one of my mentors told me that knowing when to end a project is just as good as finishing it. That made me feel a lot better about letting it go.
607 |
608 | By that time, another Python visualization library called Seaborn was becoming more popular. In my opinion, the design was better in many cases. prettyplotlib was my first big Python library, so it was rough around the edges in places. I didn't know a lot about packaging at the time. Seaborn seemed more polished. It made more sense to me to use and contribute to Seaborn, and point other people to that, rather than trying to keep prettyplotlib going.
609 |
610 | Brett: I always have multiple projects going at once, so projects that require little maintenance tend to fall off my radar. If I start getting bug reports for something I no longer have an interest in developing, I have to make a judgment call as to whether I want to invest in the update. I usually make the decision based on its adoption rate, but also my level of motivation. If I have evidence that more than 50 people are actively using the project, it's probably worth a bug fix, assuming it's not an inordinate amount of work. If I've lost interest, it's a rewrite, and/or less than 50 people are using it, it's probably time to retire it or find someone from the community willing to take it over. Projects that rely on APIs and other outside applications often require more work than is worthwhile once things start to break. Historically, those are the projects that get retired the fastest. Also, there are different degrees of sunsetting. In some cases, I might not actively maintain something, but I still accept pull requests from other people.
611 |
612 | The case of nvALT is a little different. I just couldn’t justify the amount of unpaid work that went into nvALT, so I decided to develop a new, commercial version called nvULTRA, rebuilt from the ground up with all-new code. The nvALT source code will still be available, but unmaintained.
613 |
614 | Klint: Once you’ve made the decision, what are the early steps of deprecating a project? How did you communicate with users about the end?
615 |
616 | Brett: I try to give at least 30 days notice when retiring a project, and offer alternatives early, if possible. Even if I'm immediately done working on a project, I leave the 30-day window open to take care of issues and help users transition. Most of my utilities are distributed through package managers, so end-of-life statements are sent through there. I always make a blog announcement, and if I know I’m not touching it anymore, I'll update the README and archive the repository.
617 |
618 | Olga: I spread word through a blog post and a tweet announcing that I wasn't going to actively fix bugs anymore and pointed people to Seaborn instead. prettyplotlib wasn't a huge project with zillions of contributors or users, it was a small community. People were pretty supportive of my decision, which was a relief.
619 |
620 | Ben: I got the info out there by making an announcement in the BoltDB README. I had already announced that the project was feature-complete, so I don't think it came as a big surprise to people. I tweeted it out and word spread quickly. It was on Reddit, it was on Hacker News. If anyone wanted a replacement I pointed them to the BBolt fork.
621 |
622 | |
623 |
624 |
625 |
626 | I usually make the decision based on its adoption rate, but also my level of motivation. If I have evidence that more than 50 people are actively using the project, it's probably worth a bug fix, assuming it's not an inordinate amount of work.
627 |
628 | - Brett Terpstra, maintainer of Slogger and Doing
629 | |
630 |
631 |
632 |
633 | Klint: It looks like for the most part you’ve all kept your code online, as opposed to deleting the repos. Why is that? Is there a scenario where you'd suggest taking code offline, as opposed to just archiving it?
634 |
635 | Olga: I didn't see any reason not to leave the code up. It was used for research and needs to be able to be shared so people can reproduce their results, so it really didn't make sense to take the code away from people, so long as they know that it's not still being maintained. Anyone thinking about taking their software offline should consider whether they might be creating reproducibility problems for people in science and academia.
636 |
637 | Ben: The only reason I can think of to take code down is if it's somehow bad or dangerous. For example, if you realized that you made something that was actually morally wrong. But I think most code is morally ambiguous, neither good nor bad.
638 |
639 | Brett: I generally keep code available unless it presents an active security risk. If people want to fork and improve it, I'll ask that they keep me updated and I’ll link to their work. A lot of my utilities include techniques and methods that I think could be valuable to someone trying to solve a related problem. I often use GitHub similar to the way folks use StackOverflow: to look for a solution to a particular problem buried in a repository's code. I want my solutions to be available for that purpose, even if the bulk of the code is no longer maintained, or possibly doesn't function anymore. And it’s obviously much easier for someone to fork an unmaintained project if I keep the code online.
640 |
641 | Klint: Is there anything you'd do differently if you could go back? Or are there any common mistakes you'd warn other people about?
642 |
643 | Olga: I don't know if I would do anything differently, but if there was one thing I would tell my younger self, it would be that people will find alternatives. You'll be fine, the world will be fine. You should be proud of what you did. The guilt doesn't help anyone.
644 |
645 | Ben: I agree. I would have deprecated BoltDB a good year or two earlier. Many people feel obligated to keep maintaining things, especially if it's the first time you've had an open source project take off. It gets easier to let go as time goes on. As far as mistakes other people have made: I don't think it's helpful to throw a fit and just take your code offline. I understand being frustrated by open source and needing to walk away from projects for mental health reasons, but taking your work offline has negative consequences for others.
646 |
647 | Brett: These days I make sure from the very beginning that I have a built-in way to notify users of end-of-life. That way I can effectively notify everyone actively using a tool with the sunset announcement and recommended alternatives. I wish I could go back and do this for all of my old projects as well.
648 |
649 | |
650 |
651 |
652 |
653 |
654 |
655 | Poll Request
656 | |
657 |
658 |
659 | |
660 |
661 |
662 | The conversation doesn't end here! We want to know what you think. Share your thoughts in the poll below, and be sure to check out the next edition of the newsletter to see the results.
663 |
664 |
665 |
666 | Poll
667 | Which of the following is most important to you when assembling a tech stack for a new project?
668 |
669 |
670 | |
671 |
672 |
673 | |
674 |
675 |
676 |
677 |
678 |
679 | More from The ReadME Project
680 | |
681 |
682 |
683 | |
684 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 | |
694 |
695 |
696 |
697 |
698 |
699 | Code like it’s 1995
700 | Is Java dead or hitting its stride? We’ll dig into that, hear GitHub CEO Thomas Dohmke share the key to developer happiness, and more.
701 |
702 |
703 | Listen Now |
704 |
705 |
706 | |
707 |
708 |
709 | |
710 |
711 |
712 | |
713 |
714 |
715 |
716 |
717 | |
718 |
719 |
720 |
721 |
722 |
723 |
724 | |
725 |
726 |
727 | |
728 |
729 |
730 |
731 |
732 |
733 | Klint Finley
734 | Open source is democratizing video game development
735 |
736 |
737 | Read More |
738 |
739 |
740 | |
741 |
742 |
743 | |
744 |
745 | ALEXANDRA SUNDERLAND
746 | The impact of culture on code
747 |
748 |
749 | Read More |
750 |
751 |
752 | |
753 |
754 |
755 | |
756 |
757 |
758 |
759 | DAVE FARLEY
760 | What is “engineering for software?”
761 |
762 |
763 | Read More |
764 |
765 |
766 | |
767 |
768 |
769 | |
770 |
771 | LEO STOLYAROV
772 | Working across borders to achieve more
773 |
774 |
775 | Read More |
776 |
777 |
778 | |
779 |
780 |
781 | |
782 |
783 |
784 |
785 | |
786 |
787 |
788 |
789 |
790 |
791 |
792 |
793 |
794 |
795 |
796 |
797 |
798 |
799 |
800 |
801 |
802 |
803 |
804 |
805 |
806 |
807 |
808 |
809 |
810 |
811 |
812 |
813 |
814 |
815 |
816 |
817 |
818 |
819 |
820 |
821 |
822 |
823 |
824 |
825 |
826 |
827 |
828 |
829 |
830 |
831 |
832 |
833 |
834 |
835 |
836 |
837 |
838 |
839 |
840 |
841 |
842 |
843 |
844 |
845 |
846 |
847 |
848 |
849 |
850 |
851 |
852 |
853 |
854 |
855 |
856 |
857 |
858 |
859 |
860 |
861 |
862 |
863 |
864 |
865 |
866 |
867 |
868 |
869 |
870 |
871 |
872 |
873 |
874 |
875 |
876 |
877 |
878 |
879 |
880 |
881 |
882 |
883 |
884 |
885 |
886 |
887 |
888 |
889 |
890 |
891 |
892 |
893 |
894 |
895 |
896 |
897 |
898 |
899 |
900 |
901 |
902 |
903 |
904 |
905 |
906 |
907 |
908 |
909 |
910 |
911 |
912 |
913 | |
914 |
915 |
916 |
917 |
918 |
919 |
920 |
921 |
922 | Contribute to The ReadME Project
923 | The ReadME Project is powered by a thriving and collaborative open source community. We're always on the lookout for developers, maintainers, and experts to feature across all story types. Know someone who should be featured? Have a story idea that demonstrates the impact of OSS on deep tech? Have a best practice that should be shared? Let us know!
924 |
931 | If you were forwarded this email and would like to continue receiving this monthly newsletter, sign up here.
932 | |
933 |
934 |
935 | |
936 |
937 |
938 |
939 |
940 |
941 |
942 |
943 |
944 |
945 |
969 | |
970 |
971 |
972 |
973 |
974 |
975 |
976 | |
977 |
978 |
979 | |
980 |
981 |
982 |
983 |
984 |
985 | Copyright © 2022 GitHub, Inc All rights reserved.
986 |
987 | Our mailing address is:
988 | GitHub, Inc 88 Colin P Kelly Jr St San Francisco, CA 94107-2008 USA
989 |
990 | Want to change how you receive these emails?
991 | You can update your preferences or unsubscribe from this list. |
992 |
993 |
994 | |
995 |
996 |
997 | |
998 |
999 |
1000 |
1001 |
1002 |
1003 |
1004 |
1005 |
1006 |
1007 |
1008 |
--------------------------------------------------------------------------------