improve: max occurrence limit
This commit is contained in:
45
邮件批量发送脚本.py
45
邮件批量发送脚本.py
@@ -13,19 +13,22 @@ from datetime import timedelta
|
||||
|
||||
parser = argparse.ArgumentParser(description="邮件批量发送脚本")
|
||||
parser.add_argument('datasheet')
|
||||
parser.add_argument('--column', type=str, nargs='?', default='邮箱')
|
||||
parser.add_argument('--column-address', type=str, nargs='?', default='邮箱')
|
||||
parser.add_argument('--column-code', type=str, nargs='?', default='客户编号')
|
||||
parser.add_argument('--address', type=str, required=True)
|
||||
parser.add_argument('--password', type=str, required=True)
|
||||
parser.add_argument('--encoding', type=str, nargs='?', default='utf-8')
|
||||
parser.add_argument('--timeout', type=int, nargs='?', default=60)
|
||||
parser.add_argument('--interval', type=int, nargs='?', default=5)
|
||||
parser.add_argument('--rate-limit', type=int, nargs='?', default=8.33)
|
||||
parser.add_argument('--max-occurrence', type=int, nargs='?', default=0)
|
||||
|
||||
args = parser.parse_args()
|
||||
date = datetime.now()
|
||||
|
||||
sent = 0
|
||||
errors = 0
|
||||
warnings = 0
|
||||
|
||||
def main():
|
||||
# 读取收件人列表
|
||||
@@ -33,8 +36,11 @@ def main():
|
||||
try:
|
||||
with open(args.datasheet, 'r', encoding=args.encoding) as file:
|
||||
rows = csv.DictReader(file)
|
||||
recipients = [row[args.column] for row in rows]
|
||||
print(f'[信息] 已读取联系人信息共 {len(recipients)} 条')
|
||||
data = list(rows)
|
||||
|
||||
rate = 60 / (args.interval + 3)
|
||||
limit = len(data)
|
||||
print(f'[信息] 已读取联系人信息共 {limit} 条')
|
||||
except Exception as e:
|
||||
print(f'[!!!!] 读取数据表失败:{e}')
|
||||
return 1
|
||||
@@ -93,11 +99,8 @@ def main():
|
||||
print(f'[!!!!] 读取邮件时发生了错误:{e}')
|
||||
return 5
|
||||
|
||||
rate = 60 / (args.interval + 3)
|
||||
limit = len(recipients)
|
||||
|
||||
print(f'[信息] 当前发送速率 {round(rate, 2)} 封/分钟')
|
||||
print(f'[信息] 预计使用时间 {timedelta(seconds=rate * limit)}')
|
||||
print(f'[信息] 预计使用时间 {timedelta(minutes=limit / rate)}')
|
||||
|
||||
if rate > args.rate_limit:
|
||||
print(f'[警告] 已设置速率限制 {round(args.rate_limit, 2)}')
|
||||
@@ -114,7 +117,25 @@ def main():
|
||||
global date
|
||||
date = datetime.now()
|
||||
|
||||
for recipient in recipients:
|
||||
current = None
|
||||
occurrence = 0
|
||||
|
||||
for row in data:
|
||||
recipient = row.get(args.column_address)
|
||||
code = row.get(args.column_code)
|
||||
|
||||
if code == current:
|
||||
occurrence += 1
|
||||
else:
|
||||
current = code
|
||||
occurrence = 1
|
||||
|
||||
if args.max_occurrence > 0 and occurrence > args.max_occurrence:
|
||||
print(f'[警告] 收件人 {recipient} 所属组织已重复出现 {occurrence} 次')
|
||||
global warnings
|
||||
warnings += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
print(f'[信息] 正在发送:{recipient}')
|
||||
|
||||
@@ -137,13 +158,12 @@ def main():
|
||||
|
||||
global sent
|
||||
sent += 1
|
||||
time.sleep(args.interval)
|
||||
except Exception as e:
|
||||
print(f'[!!!!] 发生错误:{e}')
|
||||
return 6
|
||||
|
||||
# 等待邮件发送
|
||||
locate(driver, EC.invisibility_of_element_located, (By.CSS_SELECTOR, "div.mail-send-progress.bottom-message"))
|
||||
time.sleep(args.interval)
|
||||
|
||||
# 检测页面警告
|
||||
try: alert = driver.find_element(By.CSS_SELECTOR, "div.io-ox-alert.io-ox-alert-error")
|
||||
@@ -155,6 +175,9 @@ def main():
|
||||
global errors
|
||||
errors += 1
|
||||
|
||||
# 发送失败不计入重复项
|
||||
if occurrence > 1: occurrence -= 1
|
||||
|
||||
# 关闭警告
|
||||
button = locate(alert, EC.element_to_be_clickable, (By.CSS_SELECTOR, "button[data-action='close']"))
|
||||
button.click()
|
||||
@@ -189,6 +212,6 @@ except KeyboardInterrupt:
|
||||
print('[信息] 程序中断')
|
||||
status = 145
|
||||
|
||||
print(f'[信息] 已发送 {sent} 封;发送失败 {errors} 封')
|
||||
print(f'[信息] 已发送 {sent} 封;发送失败 {errors} 封;跳过重复项 {warnings} 个')
|
||||
print(f'[信息] 总耗时 {str(datetime.now() - date)}')
|
||||
exit(status)
|
||||
|
||||
161
邮件编辑器.html
Normal file
161
邮件编辑器.html
Normal file
@@ -0,0 +1,161 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet">
|
||||
<link href="http://antifavicon.com/ex8.png" rel="icon">
|
||||
<title>编辑邮件</title>
|
||||
<style>
|
||||
body {
|
||||
height: 85vh;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
max-height: fit-content;
|
||||
}
|
||||
#subject {
|
||||
border: 0px;
|
||||
border-left: 1px solid #CCC;
|
||||
border-right: 1px solid #CCC;
|
||||
padding: 10.5px 16px 10.5px 16px;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
line-height: calc(1.5 / 1);
|
||||
}
|
||||
input:-webkit-autofill {
|
||||
-webkit-box-shadow: 0 0 0px 1000px white inset;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="toolbar">
|
||||
<span class="ql-formats">
|
||||
<!-- Add font size dropdown -->
|
||||
<select class="ql-font">
|
||||
</select>
|
||||
<select class="ql-header">
|
||||
<option value="1"></option>
|
||||
<option value="2"></option>
|
||||
<option value="3"></option>
|
||||
<option value="4"></option>
|
||||
<!-- Note a missing, thus falsy value, is used to reset to default -->
|
||||
<option selected></option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<button class="ql-bold"></button>
|
||||
<button class="ql-italic"></button>
|
||||
<button class="ql-underline"></button>
|
||||
<button class="ql-strike"></button>
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<button class="ql-script" value="sub"></button>
|
||||
<button class="ql-script" value="super"></button>
|
||||
<button class="ql-list" value="ordered"></button>
|
||||
<button class="ql-list" value="bullet"></button>
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<button id="load">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="oklch(0.269 0 0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" />
|
||||
<path d="M12 11l0 6" />
|
||||
<path d="M9 14l6 0" />
|
||||
</svg>
|
||||
</button>
|
||||
<button id="save">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="oklch(0.269 0 0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2" />
|
||||
<path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M14 4l0 4l-6 0l0 -4" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<input id="subject" name="subject" type="text" />
|
||||
<div id="editor"></div>
|
||||
</body>
|
||||
<script>
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: '#toolbar'
|
||||
}
|
||||
});
|
||||
|
||||
const loadButton = document.querySelector("#load");
|
||||
const saveButton = document.querySelector('#save');
|
||||
const editor = document.querySelector('#editor');
|
||||
const input = document.querySelector('#subject');
|
||||
|
||||
let reader = new FileReader();
|
||||
let parser = new DOMParser();
|
||||
let fileHandle = 'desktop';
|
||||
|
||||
reader.addEventListener('load', () => {
|
||||
const contents = reader.result;
|
||||
const virtual = parser.parseFromString(contents, 'text/html');
|
||||
|
||||
const body = virtual.querySelector('body');
|
||||
const html = body ? body.innerHTML : contents;
|
||||
|
||||
const delta = quill.clipboard.convert({ html });
|
||||
quill.setContents(delta, 'silent');
|
||||
});
|
||||
|
||||
loadButton.addEventListener('click', async () => {
|
||||
[fileHandle] = await window.showOpenFilePicker({
|
||||
startIn: fileHandle,
|
||||
types: [
|
||||
{ accept: { "text/html": [".html"] }},
|
||||
],
|
||||
});
|
||||
const file = await fileHandle.getFile();
|
||||
const name = fileHandle.name;
|
||||
const subject = name.substring(0, name.lastIndexOf('.'));
|
||||
|
||||
input.value = subject;
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
saveButton.addEventListener('click', async () => {
|
||||
fileHandle = await window.showSaveFilePicker({
|
||||
startIn: fileHandle,
|
||||
suggestedName: input.value,
|
||||
types: [
|
||||
{ accept: { "text/html": [".html"] }},
|
||||
],
|
||||
});
|
||||
let writable = await fileHandle.createWritable();
|
||||
let contents = editor.firstChild.innerHTML;
|
||||
|
||||
const name = fileHandle.name;
|
||||
const subject = name.substring(0, name.lastIndexOf('.'));
|
||||
input.value = subject;
|
||||
|
||||
let html = document.createElement('html');
|
||||
let body = document.createElement('body');
|
||||
let meta = document.createElement('meta');
|
||||
let link = document.createElement('link');
|
||||
|
||||
if (!(/<html>[\s\S]*<\/html>/i).test(contents)) {
|
||||
meta.setAttribute('charset', "UTF-8");
|
||||
link.setAttribute('href', "https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css");
|
||||
link.setAttribute('rel', "stylesheet");
|
||||
body.setAttribute('class', "ql-container ql-editor ql-snow");
|
||||
body.innerHTML = contents;
|
||||
html.appendChild(meta);
|
||||
html.appendChild(link);
|
||||
html.appendChild(body);
|
||||
contents = html.outerHTML;
|
||||
}
|
||||
|
||||
await writable.write(contents);
|
||||
await writable.close();
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
Reference in New Issue
Block a user