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