update: bump 'common' version to 0.1.20

This commit is contained in:
2026-08-11 21:18:44 +08:00
parent ddaca17173
commit a9c8644f03
3 changed files with 109 additions and 118 deletions
+13 -20
View File
@@ -191,15 +191,14 @@ import { default as $, LogRecord, Rpc2 } from '/';
let major = 0; let major = 0;
let minor = 0; let minor = 0;
let busy = false; let busy = false;
let save = false;
let status = null; let status = null;
let locale = null; let locale = null;
let unique = null; let unique = null;
let writable = null; let filehandle = null;
let filereader = null;
let subcategories = []; let subcategories = [];
window.addEventListener('beforeunload', (e) => { window.addEventListener('beforeunload', (e) => {
writable?.abort();
Rpc2.notify('exit'); Rpc2.notify('exit');
e.preventDefault(); e.preventDefault();
e.returnValue = ''; e.returnValue = '';
@@ -214,7 +213,7 @@ document.title += ` (${name})`;
async function handlePrimaryButtonClick() { async function handlePrimaryButtonClick() {
switch (status) { switch (status) {
case 'IDLE': case 'IDLE':
let PickerOptions = { [filehandle] = await showOpenFilePicker({
types: [ types: [
{ {
description: "Excel Spreadsheet", description: "Excel Spreadsheet",
@@ -223,17 +222,11 @@ async function handlePrimaryButtonClick() {
} }
} }
] ]
}; });
let [handle] = await showOpenFilePicker(PickerOptions); let chain = filehandle.getFile().then(f => Promise.all([f, f.arrayBuffer()]));
filereader = () => handle.getFile().then(file => new Promise((resolve, reject) => { let [file, buffer] = await chain.catch(e => alert('(ERROR) ' + new String(e ?? 'Failed to load file')));
let reader = new FileReader();
reader.onload = () => resolve([file, reader.result]);
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
}));
let [file, buffer] = await filereader().catch(e => alert('(ERROR) ' + new String(e ?? 'Unable to read data'))); save = await handle.requestPermission({ mode: 'readwrite' }) === 'granted';
writable = await handle.createWritable().catch(() => null);
major = await Rpc2.invoke('loads', { params: [file.name, new Uint8Array(buffer).toBase64()] }); major = await Rpc2.invoke('loads', { params: [file.name, new Uint8Array(buffer).toBase64()] });
$.get('#fileLabel').innerText = file.name; $.get('#fileLabel').innerText = file.name;
$.get('#fromLabel').innerText = parameters.account; $.get('#fromLabel').innerText = parameters.account;
@@ -241,7 +234,7 @@ async function handlePrimaryButtonClick() {
break; break;
case 'READY': case 'READY':
let locale = $('#locale').value; let locale = $('#locale').value;
let options = { locale, subcategories, mapping: new Object(), writable: new Boolean(writable), start: 2, limit: null }; let options = { locale, subcategories, save, mapping: new Object(), start: 2, limit: null };
for (let element of $.all("input[type='number'].params")) { for (let element of $.all("input[type='number'].params")) {
parameters[element.id] = element.disabled ? null : element.valueAsNumber; parameters[element.id] = element.disabled ? null : element.valueAsNumber;
@@ -422,7 +415,6 @@ while (await new Promise(o => setTimeout(o, 1000, true))) {
case 'IDLE': case 'IDLE':
$('#send > span.text').innerText = 'Open'; $('#send > span.text').innerText = 'Open';
$('#send').classList.remove('pulse'); $('#send').classList.remove('pulse');
await writable?.abort().catch(() => void 0).finally(() => writable = null);
break; break;
case 'READY': case 'READY':
$('#send > span.text').innerText = 'Send'; $('#send > span.text').innerText = 'Send';
@@ -431,17 +423,18 @@ while (await new Promise(o => setTimeout(o, 1000, true))) {
$('#remainingLabel').innerText = ''; $('#remainingLabel').innerText = '';
break; break;
case 'CLOSING': case 'CLOSING':
if (writable) try { if (save) try {
$.all('#actions > button').forEach(e => e.disabled = true); $.all('#actions > button').forEach(e => e.disabled = true);
let data = await filereader().then(([_, b]) => Rpc2.invoke('merge', { params: [new Uint8Array(b).toBase64()] })); let writable = await filehandle.createWritable({ mode: 'exclusive' });
let data = await filehandle.getFile().arrayBuffer().then(b => Rpc2.invoke('merge', { params: [new Uint8Array(b).toBase64()] }));
await writable.write(Uint8Array.fromBase64(data)); await writable.write(Uint8Array.fromBase64(data));
await writable.close(); await writable.close();
await Rpc2.notify('close'); await Rpc2.notify('close');
} catch (e) { } catch (e) {
if (Error.isError(e) && e.name === 'NoModificationAllowedError') continue;
alert('(ERROR) ' + new String(e ?? 'File operation failed')); alert('(ERROR) ' + new String(e ?? 'File operation failed'));
await writable.abort().catch(() => void 0);
} }
writable = null; save = false;
continue; continue;
case 'RUNNING': case 'RUNNING':
$('#send > span.text').innerText = 'Pause'; $('#send > span.text').innerText = 'Pause';
+96 -98
View File
@@ -43,15 +43,15 @@ WEBMAIL = "https://email.ionos.fr/appsuite/#app=io.ox/mail&mailto=%s"
def main(driver: Chrome, logger = logging.getLogger('main')): def main(driver: Chrome, logger = logging.getLogger('main')):
timer = Timer() timer = Timer()
parameters = vars(args) parameters = vars(args)
sp = ServiceProvider.default() sp = ServiceProvider(ServiceProvider.Options(application='Mailer'))
class Status(Enum): class Status(Enum):
IDLE = 0 IDLE = 0
READY = 1 READY = 1
RUNNING = 2 RUNNING = 2
STANDBY = 3 STANDBY = 3
CLOSING = 4 CLOSING = 4
class ColumnMapping: class ColumnMapping:
def __init__(self, email=None, recipient=None, code=None, region=None, sent=None, variables=None): def __init__(self, email=None, recipient=None, code=None, region=None, sent=None, variables=None):
self.email = email or '邮箱' self.email = email or '邮箱'
@@ -60,7 +60,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
self.region = region or '国家地区' self.region = region or '国家地区'
self.sent = sent or '已发送' self.sent = sent or '已发送'
self.variables = variables or '变量值' self.variables = variables or '变量值'
status = Status.IDLE status = Status.IDLE
options = dict() options = dict()
headers = list() headers = list()
@@ -69,7 +69,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
filename: str = None filename: str = None
column: ColumnMapping = None column: ColumnMapping = None
limit = 0 limit = 0
def begin(opts: dict, args: dict): def begin(opts: dict, args: dict):
nonlocal status nonlocal status
if status != Status.READY: raise ValueError(status) if status != Status.READY: raise ValueError(status)
@@ -78,32 +78,32 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
parameters.update(args) parameters.update(args)
timer.clear() timer.clear()
timer.start() timer.start()
def pause(): def pause():
nonlocal status nonlocal status
status = Status.STANDBY status = Status.STANDBY
timer.pause() timer.pause()
def resume(): def resume():
nonlocal status nonlocal status
status = Status.RUNNING status = Status.RUNNING
driver.switch_to.window(driver.current_window_handle) driver.switch_to.window(driver.current_window_handle)
timer.start() timer.start()
def unique(header: str): def unique(header: str):
result = dict() result = dict()
col = headers.index(header) + 1 col = headers.index(header) + 1
for row in range(options.get('start'), limit + 1): for row in range(options.get('start'), limit + 1):
key = str(wb.active.cell(row, col).value) key = str(wb.active.cell(row, col).value)
result[key] = result.get(key, 0) + 1 result[key] = result.get(key, 0) + 1
return result return result
sp.add(begin, pause, resume, unique) sp.add(begin, pause, resume, unique)
sp.set('status', lambda: status.name) sp.set('status', lambda: status.name)
sp.set('uptime', lambda: timer.delta()) sp.set('uptime', lambda: timer.delta())
try: try:
mapping = ColumnMapping(**{ mapping = ColumnMapping(**{
k.lower().strip(): v.strip() for k, v in map(lambda o: str.split(o, '=', 2), parameters['column'] or list()) k.lower().strip(): v.strip() for k, v in map(lambda o: str.split(o, '=', 2), parameters['column'] or list())
@@ -111,7 +111,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
except Exception as e: except Exception as e:
logger.critical('Unable to load column mappings', exc_info=e) logger.critical('Unable to load column mappings', exc_info=e)
return 1 return 1
class Locale: class Locale:
def __init__(self, name, timezone, default, morning=None, afternoon=None, evening=None, keywords=None): def __init__(self, name, timezone, default, morning=None, afternoon=None, evening=None, keywords=None):
self.name: str = name self.name: str = name
@@ -121,7 +121,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
self.afternoon: str = afternoon self.afternoon: str = afternoon
self.evening: str = evening self.evening: str = evening
self.keywords: list[str] = keywords self.keywords: list[str] = keywords
locales = [ locales = [
Locale("en", "Europe/London", "Hello" , "Good morning", "Good afternoon" , "Good evening", []), Locale("en", "Europe/London", "Hello" , "Good morning", "Good afternoon" , "Good evening", []),
Locale("fr", "Europe/Paris" , "Bonjour", None , None , "Bonsoir" , ['法国', '比利时', '留尼汪']), Locale("fr", "Europe/Paris" , "Bonjour", None , None , "Bonsoir" , ['法国', '比利时', '留尼汪']),
@@ -130,29 +130,29 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
Locale("es", "Europe/Madrid", "Hola" , "Buenos días" , "Buenas tardes" , None , ['西班牙']), Locale("es", "Europe/Madrid", "Hola" , "Buenos días" , "Buenas tardes" , None , ['西班牙']),
Locale("pt", "Europe/Lisbon", "Olá" , "Bom dia" , "Boa tarde" , None , ['葡萄牙']), Locale("pt", "Europe/Lisbon", "Olá" , "Bom dia" , "Boa tarde" , None , ['葡萄牙']),
] ]
try: try:
sp.set('context', lambda: { 'locales': list(map(vars, locales)), 'mapping': vars(mapping), 'parameters': parameters }) sp.set('context', lambda: { 'locales': list(map(vars, locales)), 'mapping': vars(mapping), 'parameters': parameters })
driver.get(sp.run()) driver.get(sp.run())
except Exception as e: except Exception as e:
logger.critical('Unable to load starup page', exc_info=e) logger.critical('Unable to load starup page', exc_info=e)
return 2 return 2
def contains_non_latin_alphabet(string: str): def contains_non_latin_alphabet(string: str):
from unicodedata import category, name from unicodedata import category, name
for char in string: for char in string:
if char.isdigit() or (category(char).startswith('L') and not name(char, '').startswith('LATIN')): if char.isdigit() or (category(char).startswith('L') and not name(char, '').startswith('LATIN')):
return True return True
return False return False
def ready(driver: Chrome): def ready(driver: Chrome):
busy = driver.find_element(By.CSS_SELECTOR, ".io-ox-busy") busy = driver.find_element(By.CSS_SELECTOR, ".io-ox-busy")
return busy return busy
def catch(predicate: Callable[[Chrome], WebElement]): def catch(predicate: Callable[[Chrome], WebElement]):
wait = WebDriverWait(driver, timeout=parameters['interval']) wait = WebDriverWait(driver, timeout=parameters['interval'])
return wait.until(predicate, 'Timeout') return wait.until(predicate, 'Timeout')
try: try:
driver.switch_to.new_window('tab') driver.switch_to.new_window('tab')
driver.set_page_load_timeout(parameters['timeout']) driver.set_page_load_timeout(parameters['timeout'])
@@ -160,33 +160,33 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
except TimeoutException: except TimeoutException:
logger.warning('Timeout') logger.warning('Timeout')
driver.execute_script("window.stop();") driver.execute_script("window.stop();")
try: try:
setup(driver, parameters) setup(driver, parameters)
click("#selectAll") click("#selectAll")
except Exception as e: except Exception as e:
logger.debug('Can I haz cheez burger? :3', exc_info=e) logger.debug('Can I haz cheez burger? :3', exc_info=e)
try: try:
logger.info('Logging in as %s', parameters['account']) logger.info('Logging in as %s', parameters['account'])
username = locate("#username") username = locate("#username")
username.send_keys(parameters['account']) username.send_keys(parameters['account'])
click("#button--with-loader") click("#button--with-loader")
password = locate("#password") password = locate("#password")
password.send_keys(parameters['password']) password.send_keys(parameters['password'])
click("#button--with-loader") click("#button--with-loader")
except Exception as e: except Exception as e:
logger.critical('Error while logging in to %s', LOGIN, exc_info=e) logger.critical('Error while logging in to %s', LOGIN, exc_info=e)
return 3 return 3
while True: while True:
try: try:
loader = locate("#background-loader", wait=False) loader = locate("#background-loader", wait=False)
if not loader.is_displayed(): break if not loader.is_displayed(): break
except: except:
sleep(1) sleep(1)
def get_subject(): def get_subject():
try: try:
element = locate("h1.subject", wait=False) element = locate("h1.subject", wait=False)
@@ -194,7 +194,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
return subject return subject
except: except:
return None return None
def get_address(): def get_address():
try: try:
element = locate("header div.from", wait=False) element = locate("header div.from", wait=False)
@@ -202,11 +202,11 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
return address return address
except: except:
return None return None
def cell(row: int, header: str): def cell(row: int, header: str):
result = wb.active.cell(row, headers.index(header) + 1) result = wb.active.cell(row, headers.index(header) + 1)
return result return result
try: try:
click("li[data-id='default0/Brouillons']", condition=EC.presence_of_element_located) click("li[data-id='default0/Brouillons']", condition=EC.presence_of_element_located)
click("button[data-id='default0/Brouillons']", condition=EC.presence_of_element_located) click("button[data-id='default0/Brouillons']", condition=EC.presence_of_element_located)
@@ -216,74 +216,74 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
logger.warning("Could not open drafts; this may cause issues", exc_info=e) logger.warning("Could not open drafts; this may cause issues", exc_info=e)
finally: finally:
driver.switch_to.window(driver.window_handles[0]) driver.switch_to.window(driver.window_handles[0])
class Wait(Action): class Wait(Action):
@classmethod @classmethod
def prepare(cls): def prepare(cls):
return True return True
@classmethod @classmethod
def perform(cls): def perform(cls):
if status == Status.RUNNING: return False if status == Status.RUNNING: return False
sleep(0.2); return True sleep(0.2); return True
class Timeout(Action): class Timeout(Action):
timer = Timer() clock = Timer()
@classmethod @classmethod
def prepare(cls): def prepare(cls):
cls.timer.clear() cls.clock.clear()
cls.timer.start() cls.clock.start()
return True return True
@classmethod @classmethod
def perform(cls): def perform(cls):
if cls.timer.delta() > parameters['timeout']: raise cls if cls.clock.delta() > parameters['timeout']: raise cls
sleep(0.2); return True sleep(0.2); return True
class Acknowledge(Action): class Acknowledge(Action):
@classmethod @classmethod
def prepare(cls): def prepare(cls):
return status == Status.RUNNING return status == Status.RUNNING
class Cancel(Action): class Cancel(Action):
@classmethod @classmethod
def prepare(cls): def prepare(cls):
nonlocal status nonlocal status
status = Status.RUNNING status = Status.RUNNING
return True return True
@classmethod @classmethod
def perform(cls): def perform(cls):
driver.switch_to.window(driver.window_handles[0]) driver.switch_to.window(driver.window_handles[0])
raise cls raise cls
class Close(Action): class Close(Action):
@classmethod @classmethod
def prepare(cls): def prepare(cls):
return status == Status.CLOSING return status == Status.CLOSING
class Skip(Action): class Skip(Action):
@classmethod @classmethod
def prepare(cls): def prepare(cls):
return True return True
@classmethod @classmethod
def perform(cls): def perform(cls):
driver.switch_to.window(driver.current_window_handle) driver.switch_to.window(driver.current_window_handle)
raise cls raise cls
flow = ActionFlow() flow = ActionFlow()
flow.stage(Wait, Acknowledge, Timeout, Cancel, Close, Skip) flow.stage(Wait, Acknowledge, Timeout, Cancel, Close, Skip)
sp.add(pairs=[ (k.lower(), v) for k, v in flow ]) sp.add(pairs=[ (k.lower(), v) for k, v in flow ])
sp.set('actions', lambda: flow.capabilities()) sp.set('actions', lambda: flow.capabilities())
def loads(name: str, b64: str): def loads(name: str, b64: str):
from base64 import b64decode from base64 import b64decode
from openpyxl import load_workbook from openpyxl import load_workbook
nonlocal status, filename, wb, limit nonlocal status, filename, wb, limit
if status != Status.IDLE: raise ValueError(status) if status != Status.IDLE: raise ValueError(status)
data = b64decode(b64) data = b64decode(b64)
logger.info('Received %s byte(s) total', len(data)) logger.info('Received %s byte(s) total', len(data))
logger.info('Loading...') logger.info('Loading...')
@@ -292,56 +292,56 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
wb = load_workbook(buffer) wb = load_workbook(buffer)
limit = wb.active.max_row limit = wb.active.max_row
headers.clear() headers.clear()
for col in range(1, wb.active.max_column + 1): for col in range(1, wb.active.max_column + 1):
value = str(wb.active.cell(1, col).value) value = str(wb.active.cell(1, col).value)
headers.append(value) headers.append(value)
logger.info('Done') logger.info('Done')
flow.allow(Cancel) flow.allow(Cancel)
status = Status.READY status = Status.READY
return limit return limit
def merge(b64: str): def merge(b64: str):
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from openpyxl import load_workbook from openpyxl import load_workbook
if status != Status.CLOSING: raise ValueError(status) if status != Status.CLOSING: raise ValueError(status)
data = b64decode(b64) data = b64decode(b64)
index = headers.index(column.sent) index = headers.index(column.sent)
buffer = BytesIO(data) buffer = BytesIO(data)
target = load_workbook(buffer) target = load_workbook(buffer)
for row in range(options.get('start'), limit + 1): for row in range(options.get('start'), limit + 1):
c1 = wb.active.cell(row, index + 1) c1 = wb.active.cell(row, index + 1)
c2 = target.active.cell(row, index + 1) c2 = target.active.cell(row, index + 1)
if not (c1.value and str(c1.value).strip()): if not (c1.value and str(c1.value).strip()):
if (c2.value and str(c2.value).strip()): if (c2.value and str(c2.value).strip()):
c1.value = c2.value c1.value = c2.value
io = BytesIO() io = BytesIO()
wb.save(io) wb.save(io)
result = b64encode(io.getbuffer()).decode('ascii') result = b64encode(io.getbuffer()).decode('ascii')
return result return result
sp.add(loads, merge) sp.add(loads, merge)
sp.set('progress', lambda: progress) sp.set('progress', lambda: progress)
def save(): def save():
try: try:
file = Path(parameters['directory']).joinpath(filename or f'Mailer-{datetime.now().strftime('%Y%m%d-%H%M%S-%f')}.xlsx').resolve() file = Path(parameters['directory']).joinpath(datetime.now().strftime('Mailer-%Y%m%d-%H%M%S-%f.xlsx')).resolve()
logger.info('Saving document at %s', str(file)) logger.info('Saving document at %s', str(file))
wb.save(file) wb.save(file)
except Exception as e: except Exception as e:
logger.error('Error while writing data', exc_info=e) logger.error('Error while writing data', exc_info=e)
exit = sp.pop('exit') exit = sp.pop('exit')
sp.set('exit', lambda: (save() if options.get('writable') and status.value >= 2 else None, exit())) sp.set('exit', lambda: (save() if options.get('save') and status.value >= 2 else None, exit()))
class MismatchedEmailSubject(Exception): pass class MismatchedEmailSubject(Exception): pass
class MismatchedEmailAddress(Exception): pass class MismatchedEmailAddress(Exception): pass
class Next(Exception): pass class Next(Exception): pass
while True: while True:
try: try:
flow.react(Wait) flow.react(Wait)
@@ -349,22 +349,22 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
driver.switch_to.window(driver.window_handles[1]) driver.switch_to.window(driver.window_handles[1])
subject = get_subject() subject = get_subject()
column = ColumnMapping(**options.get('mapping')) column = ColumnMapping(**options.get('mapping'))
if column.email not in headers or limit < 2: if column.email not in headers or limit < 2:
logger.error("Column '%s' is not found or does not contain valid data", column.email) logger.error("Column '%s' is not found or does not contain valid data", column.email)
raise Cancel() raise Cancel()
if column.sent not in headers: if column.sent not in headers:
wb.active.cell(1, wb.active.max_column + 1).value = column.sent wb.active.cell(1, wb.active.max_column + 1).value = column.sent
headers.append(column.sent) headers.append(column.sent)
logger.info('Read %s line(s) total', limit) logger.info('Read %s line(s) total', limit)
logger.info('Subject: %s', subject) logger.info('Subject: %s', subject)
logger.info('From: %s', parameters['account']) logger.info('From: %s', parameters['account'])
locale: Locale = next(filter(lambda o: o.name == options.get('locale'), locales)) locale: Locale = next(filter(lambda o: o.name == options.get('locale'), locales))
logger.info('Locale: %s', locale.name.upper()) logger.info('Locale: %s', locale.name.upper())
logger.info('Timezone: %s', locale.timezone) logger.info('Timezone: %s', locale.timezone)
progress['done'] = 0 progress['done'] = 0
progress['skip'] = 0 progress['skip'] = 0
progress['next'] = 0 progress['next'] = 0
@@ -378,12 +378,12 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
status = Status.IDLE status = Status.IDLE
logger.error('Error while loading data', exc_info=e) logger.error('Error while loading data', exc_info=e)
continue continue
index = 2 index = 2
attempts = 0 attempts = 0
tz = ZoneInfo(locale.timezone) tz = ZoneInfo(locale.timezone)
occurrence = dict() occurrence = dict()
while index <= limit: while index <= limit:
try: try:
flow.react(Wait) flow.react(Wait)
@@ -391,16 +391,16 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
try: code = str(cell(index, column.code).value) try: code = str(cell(index, column.code).value)
except ValueError: code = None except ValueError: code = None
occurrence.setdefault(code, 0) occurrence.setdefault(code, 0)
if (target := cell(index, column.email).value) is None or not (email := str(target).strip()): if (target := cell(index, column.email).value) is None or not (email := str(target).strip()):
logger.warning("[%d/%d] Not an email address; skipping", index-1, limit-1) logger.warning("[%d/%d] Not an email address; skipping", index-1, limit-1)
raise Skip() raise Skip()
if attempts > parameters['attempts']: if attempts > parameters['attempts']:
cell(index, column.sent).value = '' cell(index, column.sent).value = ''
logger.warning("[%d/%d] Exhausted all allowed attempts; skipping", index-1, limit-1) logger.warning("[%d/%d] Exhausted all allowed attempts; skipping", index-1, limit-1)
raise Skip() raise Skip()
if options.get('slice') and (start := options.get('start')) and (end := options.get('limit')): if options.get('slice') and (start := options.get('start')) and (end := options.get('limit')):
if index < start or index >= start + end: if index < start or index >= start + end:
logger.info("[%d/%d] Not planned; skipping '%s'", index-1, limit-1, email) logger.info("[%d/%d] Not planned; skipping '%s'", index-1, limit-1, email)
@@ -408,22 +408,20 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
if (items := options.get('subcategories')) and (item := str(cell(index, column.region).value)) not in items: if (items := options.get('subcategories')) and (item := str(cell(index, column.region).value)) not in items:
logger.info("[%d/%d] Value '%s' not enlisted; skipping '%s'", index-1, limit-1, item, email) logger.info("[%d/%d] Value '%s' not enlisted; skipping '%s'", index-1, limit-1, item, email)
raise Next() raise Next()
if (sent := cell(index, column.sent).value) is not None and str(sent).strip(): if (sent := cell(index, column.sent).value) is not None and str(sent).strip():
logger.info("[%d/%d] Already visited; skipping '%s'", index-1, limit-1, email) logger.info("[%d/%d] Already visited; skipping '%s'", index-1, limit-1, email)
occurrence[code] += 1 occurrence[code] += 1
raise Skip() raise Skip()
match options.get('occurrence'): if (caps := options.get('occurrence')) is not None and caps > 0:
case o if o is None: if code is None:
pass
case o if o > 0 and code is None:
logger.error("Column '%s' cannot be used for spam control", column.code) logger.error("Column '%s' cannot be used for spam control", column.code)
raise Cancel() raise Cancel()
case o if o > 0 and occurrence[code] >= o: if occurrence[code] >= caps:
logger.info("[%d/%d] No Spam! Skipping '%s'", index-1, limit-1, email) logger.info("[%d/%d] No Spam! Skipping '%s'", index-1, limit-1, email)
raise Skip() raise Skip()
while mails := driver.find_elements(By.CSS_SELECTOR, "div.io-ox-mail-compose-window"): while mails := driver.find_elements(By.CSS_SELECTOR, "div.io-ox-mail-compose-window"):
try: try:
click(mails[0].find_element(By.CSS_SELECTOR, "button[data-action='close']"), wait=False) click(mails[0].find_element(By.CSS_SELECTOR, "button[data-action='close']"), wait=False)
@@ -431,39 +429,39 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
except Exception as e: except Exception as e:
logger.debug('Unable to close off email compose windows', exc_info=e) logger.debug('Unable to close off email compose windows', exc_info=e)
break break
flow.allow(Skip, Cancel) flow.allow(Skip, Cancel)
flow.react(Wait) flow.react(Wait)
progress['email'] = str(email) progress['email'] = str(email)
logger.info('[%d/%d] Sending to %s', index-1, limit-1, email) logger.info('[%d/%d] Sending to %s', index-1, limit-1, email)
clean = True clean = True
error = None error = None
if (target := get_address()) != parameters['account']: if (target := get_address()) != parameters['account']:
error = MismatchedEmailAddress(target) error = MismatchedEmailAddress(target)
if (target := get_subject()) != subject: if (target := get_subject()) != subject:
error = MismatchedEmailSubject(target) error = MismatchedEmailSubject(target)
if error is not None and not options.get('force'): if error is not None and not options.get('force'):
raise error raise error
flow.react(Wait) flow.react(Wait)
click("ul.classic-toolbar button[aria-label='Edit copy']") click("ul.classic-toolbar button[aria-label='Edit copy']")
until(ready) until(ready)
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 options.get('greet') and not (clean := False): if options.get('greet') and not (clean := False):
match datetime.now(tz).hour: match datetime.now(tz).hour:
case h if 6 <= h < 12: hello = locale.morning case h if 6 <= h < 12: hello = locale.morning
case h if 12 <= h < 18: hello = locale.afternoon case h if 12 <= h < 18: hello = locale.afternoon
case h if 18 <= h < 21: hello = locale.evening case h if 18 <= h < 21: hello = locale.evening
case _: hello = None case _: hello = None
iframe = driver.switch_to.active_element iframe = driver.switch_to.active_element
action = ActionChains(driver) action = ActionChains(driver)
hello = hello or locale.default hello = hello or locale.default
if column.recipient in headers: if column.recipient in headers:
if name := str(cell(index, column.recipient).value).strip(): if name := str(cell(index, column.recipient).value).strip():
if not contains_non_latin_alphabet(name): if not contains_non_latin_alphabet(name):
@@ -472,29 +470,29 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
parts.capitalize(force=True) parts.capitalize(force=True)
short = len(parts.first) < 3 or (len(parts.first) < 5 and parts.first.endswith('.')) short = len(parts.first) < 3 or (len(parts.first) < 5 and parts.first.endswith('.'))
hello = ' '.join(filter(None, [hello, parts.title, parts.first, (parts.middle or parts.last) if short else None])) hello = ' '.join(filter(None, [hello, parts.title, parts.first, (parts.middle or parts.last) if short else None]))
hello += ',' hello += ','
action.send_keys(hello).perform() action.send_keys(hello).perform()
if elements := iframe.find_elements(By.XPATH, f'//*[contains(text(), "{hello}")]'): if elements := iframe.find_elements(By.XPATH, f'//*[contains(text(), "{hello}")]'):
target = elements[0] target = elements[0]
clean = target.text == hello clean = target.text == hello
driver.switch_to.default_content() driver.switch_to.default_content()
flow.react(Wait) flow.react(Wait)
click("div.io-ox-mail-compose-window div[data-extension-id='to'] > div.mail-input") click("div.io-ox-mail-compose-window div[data-extension-id='to'] > div.mail-input")
box = locate("div.io-ox-mail-compose-window div[data-extension-id='to'] > div.mail-input input.token-input.tt-input[tabindex='0']") box = locate("div.io-ox-mail-compose-window div[data-extension-id='to'] > div.mail-input input.token-input.tt-input[tabindex='0']")
box.send_keys(str(email) + Keys.ENTER) box.send_keys(str(email) + Keys.ENTER)
if column.variables in headers and (v := cell(index, column.variables).value) is not None: if column.variables in headers and (v := cell(index, column.variables).value) is not None:
clean = False clean = False
target = locate("div.io-ox-mail-compose-window div[data-extension-id='subject'] input") target = locate("div.io-ox-mail-compose-window div[data-extension-id='subject'] input")
template = target.get_attribute('value') template = target.get_attribute('value')
length = len(template) length = len(template)
for i, value in enumerate(str(v).split(',')): for i, value in enumerate(str(v).split(',')):
template = template.replace('$$%s' % i, value.strip()) template = template.replace('$$%s' % i, value.strip())
target.send_keys(Keys.BACKSPACE * length) target.send_keys(Keys.BACKSPACE * length)
target.send_keys(template) target.send_keys(template)
clean = target.get_attribute('value') == template clean = target.get_attribute('value') == template
@@ -502,15 +500,15 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
flow.react(Wait) flow.react(Wait)
box = locate("div.io-ox-mail-compose-window .mail-input .tokenfield .token") box = locate("div.io-ox-mail-compose-window .mail-input .tokenfield .token")
recipient = box.get_attribute('innerText').strip() recipient = box.get_attribute('innerText').strip()
if recipient != str(email): if recipient != str(email):
logger.warning('Malformed email address detected; retrying (%d)...', attempts) logger.warning('Malformed email address detected; retrying (%d)...', attempts)
continue continue
if not clean: if not clean:
logger.warning('Malformed email content detected; retrying (%d)...', attempts) logger.warning('Malformed email content detected; retrying (%d)...', attempts)
continue continue
flow.react(Wait) flow.react(Wait)
click("div.io-ox-mail-compose-window button[data-action='send']") click("div.io-ox-mail-compose-window button[data-action='send']")
except Cancel: except Cancel:
@@ -528,7 +526,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
logger.error('Unexptected error', exc_info=e) logger.error('Unexptected error', exc_info=e)
status = Status.STANDBY status = Status.STANDBY
continue continue
try: try:
alert = catch(lambda x: x.find_element(By.CSS_SELECTOR, "div.io-ox-alert.io-ox-alert-error")) alert = catch(lambda x: x.find_element(By.CSS_SELECTOR, "div.io-ox-alert.io-ox-alert-error"))
message = alert.text.replace('\n', ' ') message = alert.text.replace('\n', ' ')
@@ -544,7 +542,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
logger.error('Unexptected error', exc_info=e) logger.error('Unexptected error', exc_info=e)
status = Status.STANDBY status = Status.STANDBY
continue continue
try: try:
logger.info('Waiting for client to acknowledge...') logger.info('Waiting for client to acknowledge...')
flow.deter(Cancel, Skip, Wait) flow.deter(Cancel, Skip, Wait)
@@ -555,8 +553,8 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
except Exception as e: except Exception as e:
logger.error('RPC communication failed', exc_info=e) logger.error('RPC communication failed', exc_info=e)
sp.pop('exit')() sp.pop('exit')()
if options.get('writable'): if options.get('save'):
try: try:
logger.info('Saving document...') logger.info('Saving document...')
status = Status.CLOSING status = Status.CLOSING
@@ -567,7 +565,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
except Exception as e: except Exception as e:
logger.warning('RPC communication failed', exc_info=e) logger.warning('RPC communication failed', exc_info=e)
save() save()
flow.deter(Acknowledge, Timeout, Close) flow.deter(Acknowledge, Timeout, Close)
driver.switch_to.window(driver.window_handles[0]) driver.switch_to.window(driver.window_handles[0])
status = Status.IDLE status = Status.IDLE
@@ -592,7 +590,7 @@ if __name__ == '__main__':
"profile.default_content_setting_values.notifications": 2, "profile.default_content_setting_values.notifications": 2,
"download.default_directory": args.directory, "download.default_directory": args.directory,
}) })
with keep.presenting(): with keep.presenting():
driver = Chrome(options=opts) driver = Chrome(options=opts)
status = main(driver) status = main(driver)
BIN
View File
Binary file not shown.