update: now able to recover from mismatched subject errors
This commit is contained in:
63
index.html
63
index.html
@@ -100,18 +100,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<label for="slice" class="pure-checkbox">
|
|
||||||
<input id="slice" type="checkbox"/> Enable task slicing
|
|
||||||
</label>
|
|
||||||
<label for="greet" class="pure-checkbox">
|
<label for="greet" class="pure-checkbox">
|
||||||
<input id="greet" type="checkbox"/> Greet recipients (by proper names if possible)
|
<input id="greet" type="checkbox"/> Greet recipients (by proper names if possible)
|
||||||
</label>
|
</label>
|
||||||
<label for="save" class="pure-checkbox">
|
<label for="recovery" class="pure-checkbox">
|
||||||
<input id="save" type="checkbox" checked/> Save to file upon exit
|
<input id="recovery" type="checkbox"/> Try recover from a mismatched subject error
|
||||||
|
</label>
|
||||||
|
<label for="slice" class="pure-checkbox">
|
||||||
|
<input id="slice" type="checkbox"/> Enable task slicing
|
||||||
</label>
|
</label>
|
||||||
<label for="nospam" class="pure-checkbox">
|
<label for="nospam" class="pure-checkbox">
|
||||||
<input id="nospam" type="checkbox"/> Avoid spamming
|
<input id="nospam" type="checkbox"/> Avoid spamming
|
||||||
</label>
|
</label>
|
||||||
|
<label for="save" class="pure-checkbox">
|
||||||
|
<input id="save" type="checkbox" checked/> Save to file upon exit
|
||||||
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<br>
|
<br>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@@ -249,8 +252,18 @@ $$$('#send', 'click', async (e) => {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
try {
|
try {
|
||||||
|
let PickerOptions = {
|
||||||
|
types: [
|
||||||
|
{
|
||||||
|
description: "Excel Spreadsheet",
|
||||||
|
accept: {
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ['.xlsx']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
$('#send > span.icon').classList.remove('hidden');
|
$('#send > span.icon').classList.remove('hidden');
|
||||||
let [handle] = await showOpenFilePicker();
|
let [handle] = await showOpenFilePicker(PickerOptions);
|
||||||
let file = await handle.getFile();
|
let file = await handle.getFile();
|
||||||
|
|
||||||
let buffer = await new Promise((resolve, reject) => {
|
let buffer = await new Promise((resolve, reject) => {
|
||||||
@@ -265,7 +278,7 @@ $$$('#send', 'click', async (e) => {
|
|||||||
Status = 'READY';
|
Status = 'READY';
|
||||||
|
|
||||||
$('#fileLabel').innerHTML = file.name;
|
$('#fileLabel').innerHTML = file.name;
|
||||||
$('#uptimeLabel').dataset.timestamp = dayjs().unix();
|
$('#uptimeLabel').dataset.seconds = 0;
|
||||||
$('#send > span.text').innerHTML = 'Send';
|
$('#send > span.text').innerHTML = 'Send';
|
||||||
$('#cancel').disabled = false;
|
$('#cancel').disabled = false;
|
||||||
$('#send').disabled = true;
|
$('#send').disabled = true;
|
||||||
@@ -420,10 +433,13 @@ function main(url, parameters, locales) {
|
|||||||
$('#cancel').disabled = false;
|
$('#cancel').disabled = false;
|
||||||
break;
|
break;
|
||||||
case 'setProgress':
|
case 'setProgress':
|
||||||
let [sent, name, recipient] = args;
|
let [index, name, recipient, sent, warnings, errors] = args;
|
||||||
let label = name ? `${name} <${recipient}>` : recipient;
|
let label = name ? `${name} <${recipient}>` : recipient;
|
||||||
$('#recipientLabel').innerHTML = label;
|
$('#recipientLabel').innerHTML = label;
|
||||||
$('#progressLabel').dataset.sent = sent;
|
$('#progressLabel').dataset.sent = sent;
|
||||||
|
$('#progressLabel').dataset.index = index;
|
||||||
|
$('#progressLabel').dataset.errors = errors;
|
||||||
|
$('#progressLabel').dataset.warnings = warnings;
|
||||||
break;
|
break;
|
||||||
case 'setStatus':
|
case 'setStatus':
|
||||||
switch (args[0]) {
|
switch (args[0]) {
|
||||||
@@ -443,7 +459,8 @@ function main(url, parameters, locales) {
|
|||||||
$('#send > span.icon').classList.add('hidden');
|
$('#send > span.icon').classList.add('hidden');
|
||||||
break;
|
break;
|
||||||
case 'FINISH':
|
case 'FINISH':
|
||||||
alert('任务结束')
|
let {sent, warnings, errors} = $('#progressLabel').dataset;
|
||||||
|
alert(`${sent||0} Sent; ${warnings||0} Warning(s); ${errors||0} Error(s)`);
|
||||||
case 'CANCEL':
|
case 'CANCEL':
|
||||||
Status = null;
|
Status = null;
|
||||||
$('#send > span.text').innerHTML = 'Open';
|
$('#send > span.text').innerHTML = 'Open';
|
||||||
@@ -488,21 +505,19 @@ function main(url, parameters, locales) {
|
|||||||
$('#statusLabel').innerHTML = Status ? Status.charAt(0).toUpperCase() + Status.slice(1).toLowerCase() : 'Idle';
|
$('#statusLabel').innerHTML = Status ? Status.charAt(0).toUpperCase() + Status.slice(1).toLowerCase() : 'Idle';
|
||||||
|
|
||||||
if (Status === 'RUNNING') {
|
if (Status === 'RUNNING') {
|
||||||
let sent = Number($('#progressLabel').dataset.sent) || 0;
|
let index = Number($('#progressLabel').dataset.index) || 0;
|
||||||
let limit = Number($('#subcategory').dataset.limit) || Limit;
|
let limit = Number($('#subcategory').dataset.limit) || Limit;
|
||||||
let percentage = parseFloat((sent / limit * 100).toFixed(2));
|
let percentage = parseFloat((index / limit * 100).toFixed(2));
|
||||||
$('#progressLabel').innerHTML = `${sent} / ${limit} (${percentage} %)`;
|
$('#progressLabel').innerHTML = `${index} / ${limit} (${percentage} %)`;
|
||||||
|
|
||||||
let timestamp = Number($('#uptimeLabel').dataset.timestamp);
|
|
||||||
let uptime = dayjs.duration(dayjs().diff(dayjs.unix(timestamp)));
|
|
||||||
let rate = sent / uptime.asSeconds();
|
|
||||||
|
|
||||||
|
let uptime = dayjs.duration($('#uptimeLabel').dataset.seconds, 'second');
|
||||||
|
let rate = Number($('#progressLabel').dataset.sent) / uptime.asSeconds();
|
||||||
let spm = parseFloat(Number(rate*60).toFixed(2));
|
let spm = parseFloat(Number(rate*60).toFixed(2));
|
||||||
let speed = spm >= 1 ? `${spm} per minute` : null;
|
|
||||||
let remaining = rate > 0 ? dayjs.duration((limit - sent) / rate, 'second').humanize() : null;
|
|
||||||
|
|
||||||
$('#remainingLabel').innerHTML = [remaining, speed].filter((x) => x).join(' - ');
|
$('#remainingLabel').innerHTML = rate > 0 ? `<span>${dayjs.duration((limit - index) / rate, 'second').humanize()}</span>` : '';
|
||||||
|
$('#remainingLabel').innerHTML += spm >= 1 ? `<span class="${spm > 8.33 ? 'rate warning' : 'rate'}">${spm} per minute</span>` : '';
|
||||||
$('#uptimeLabel').innerHTML = uptime.format("HH:mm:ss");
|
$('#uptimeLabel').innerHTML = uptime.format("HH:mm:ss");
|
||||||
|
$('#uptimeLabel').dataset.seconds = uptime.asSeconds() + 1;
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
@@ -525,6 +540,12 @@ label {
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rate:not(:first-child)::before {
|
||||||
|
padding: 0em 0.4em;
|
||||||
|
content: '–';
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
.inline-flex {
|
.inline-flex {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -536,6 +557,10 @@ label {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: orangered;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
49
main.py
49
main.py
@@ -47,7 +47,6 @@ args = parser.parse_args()
|
|||||||
inbox, outbox = Queue(), Queue()
|
inbox, outbox = Queue(), Queue()
|
||||||
connection = None
|
connection = None
|
||||||
socket = None
|
socket = None
|
||||||
server = None
|
|
||||||
|
|
||||||
sent = 0
|
sent = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
@@ -84,8 +83,8 @@ class Command:
|
|||||||
return json.dumps(pack)
|
return json.dumps(pack)
|
||||||
|
|
||||||
def tell(message, exception=None, level=2):
|
def tell(message, exception=None, level=2):
|
||||||
if exception is not None: message = ': '.join([message, exception.__class__.__name__])
|
message = ': '.join(filter(None, [message, exception.__class__.__name__ if isinstance(exception, Exception) else exception]))
|
||||||
elif isinstance(exception, Exception): message += '\n' + ''.join(traceback.format_exception(exception))
|
if isinstance(exception, Exception): message += '\n' + ''.join(traceback.format_exception(exception))
|
||||||
|
|
||||||
if not outbox.is_shutdown:
|
if not outbox.is_shutdown:
|
||||||
outbox.put(Command('tell', message, level))
|
outbox.put(Command('tell', message, level))
|
||||||
@@ -143,9 +142,9 @@ def main(driver: WebDriver):
|
|||||||
except TimeoutException: continue
|
except TimeoutException: continue
|
||||||
except: break
|
except: break
|
||||||
|
|
||||||
def ready(driver, predicate):
|
def ready(driver, predicate=lambda x: x.find_element(By.CSS_SELECTOR, ".io-ox-busy")):
|
||||||
try:
|
try:
|
||||||
wait = WebDriverWait(driver, timeout=parameters.get('timeout'))
|
wait = WebDriverWait(driver, timeout=parameters.get('interval'))
|
||||||
wait.until(predicate, '操作超时')
|
wait.until(predicate, '操作超时')
|
||||||
except:
|
except:
|
||||||
return True
|
return True
|
||||||
@@ -198,11 +197,12 @@ def main(driver: WebDriver):
|
|||||||
except: continue
|
except: continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
condition = EC.presence_of_element_located
|
||||||
# 打开草稿箱
|
# 打开草稿箱
|
||||||
click("li[data-id='default0/Brouillons']", condition=EC.presence_of_element_located)
|
click("li[data-id='default0/Brouillons']", condition)
|
||||||
click("button[data-id='default0/Brouillons']", condition=EC.presence_of_element_located)
|
click("button[data-id='default0/Brouillons']", condition)
|
||||||
# 打开邮件
|
# 打开邮件
|
||||||
click("ul[aria-label='List view'] li[data-index='0']", condition=EC.presence_of_element_located)
|
click("ul[aria-label='List view'] li[data-index='0']", condition)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tell('打开草稿邮件时发生错误', e, level=1)
|
tell('打开草稿邮件时发生错误', e, level=1)
|
||||||
pass
|
pass
|
||||||
@@ -341,7 +341,7 @@ def main(driver: WebDriver):
|
|||||||
mark = sents[current]
|
mark = sents[current]
|
||||||
|
|
||||||
occurrence = occurrences.setdefault(code, [0]) if code else [0]
|
occurrence = occurrences.setdefault(code, [0]) if code else [0]
|
||||||
outbox.put(Command('setProgress', index, name, recipient))
|
outbox.put(Command('setProgress', index, name, recipient, sent, warnings, errors))
|
||||||
|
|
||||||
if mark is not None and str(mark).strip():
|
if mark is not None and str(mark).strip():
|
||||||
tell(f'已跳过项目 {recipient}')
|
tell(f'已跳过项目 {recipient}')
|
||||||
@@ -362,11 +362,32 @@ def main(driver: WebDriver):
|
|||||||
|
|
||||||
if (target := get_address()) != address:
|
if (target := get_address()) != address:
|
||||||
raise Exception(f'邮件发件地址与设定不一致\n>> {address}\n>> {target}')
|
raise Exception(f'邮件发件地址与设定不一致\n>> {address}\n>> {target}')
|
||||||
if (target := get_subject()) != subject:
|
|
||||||
raise Exception(f'邮件主题与设定不一致\n>> {subject}\n>> {target}')
|
|
||||||
|
|
||||||
click("button[aria-label='Edit copy']")
|
if (target := get_subject()) != subject:
|
||||||
ready(driver, lambda x: x.find_element(By.CSS_SELECTOR, ".io-ox-busy"))
|
exception = Exception(f'邮件主题与设定不一致\n>> {subject}\n>> {target}')
|
||||||
|
condition = EC.presence_of_element_located
|
||||||
|
if not parameters.get('recovery'): raise exception
|
||||||
|
# 打开存档
|
||||||
|
click("li[data-id='default0/Archives'] li.folder:nth-child(1)", condition)
|
||||||
|
ready(driver)
|
||||||
|
click("ul[aria-label='List view'] li[data-index='0']", condition)
|
||||||
|
click("ul.classic-toolbar button[aria-label='More actions']", condition)
|
||||||
|
# 复制邮件
|
||||||
|
click(".abs + ul.dropdown-menu a[data-action='io.ox/mail/actions/copy']", condition)
|
||||||
|
click(".modal-dialog ul.subfolders li[data-id='default0/Brouillons']", condition)
|
||||||
|
click(".modal-dialog .modal-footer button[data-action='ok']", condition)
|
||||||
|
# 返回草稿箱
|
||||||
|
click("li[data-id='default0/Brouillons']")
|
||||||
|
ready(driver)
|
||||||
|
|
||||||
|
for attempt in range(parameters.get('retry')):
|
||||||
|
click("ul[aria-label='List view'] li[data-index='0']", condition)
|
||||||
|
if get_subject() == subject: break
|
||||||
|
time.sleep(parameters.get('interval'))
|
||||||
|
if not get_subject() == subject: raise exception
|
||||||
|
|
||||||
|
click("ul.classic-toolbar button[aria-label='Edit copy']")
|
||||||
|
ready(driver)
|
||||||
locate("div.io-ox-mail-compose-window iframe", condition=EC.frame_to_be_available_and_switch_to_it)
|
locate("div.io-ox-mail-compose-window iframe", condition=EC.frame_to_be_available_and_switch_to_it)
|
||||||
|
|
||||||
if parameters.get('greet') and greetings and timezone:
|
if parameters.get('greet') and greetings and timezone:
|
||||||
@@ -516,7 +537,7 @@ async def handler(request: ws.WebSocketRequest):
|
|||||||
inbox.shutdown(immediate=True)
|
inbox.shutdown(immediate=True)
|
||||||
|
|
||||||
async def backend(listen='127.0.0.1', port=0):
|
async def backend(listen='127.0.0.1', port=0):
|
||||||
global server, socket
|
global socket
|
||||||
listeners = await trio.open_tcp_listeners(port, host=listen)
|
listeners = await trio.open_tcp_listeners(port, host=listen)
|
||||||
server = ws.WebSocketServer(handler, listeners, max_message_size=125_000_000)
|
server = ws.WebSocketServer(handler, listeners, max_message_size=125_000_000)
|
||||||
socket = listeners[0].socket
|
socket = listeners[0].socket
|
||||||
|
|||||||
Reference in New Issue
Block a user