"""MailDock v1 example client, Python 3.10+, standard library only. Set MAILDOCK_API_KEY and optionally MAILDOCK_BASE_URL. Running this file without arguments only reads balance. Persist idempotency keys before calling create(). No POST is retried automatically. A wait timeout never cancels or buys again. """ import argparse import json import os import re import sys import time from urllib.error import HTTPError, URLError from urllib.parse import urlencode, urlsplit from urllib.request import HTTPRedirectHandler, Request, build_opener from uuid import UUID DEFAULT_BASE = 'https://mail.jqiudata.com' class APIError(RuntimeError): def __init__(self, status, code, message, retry_after=60): super().__init__(f'HTTP {status} {code}: {message}') self.status, self.code, self.retry_after = status, code, retry_after class RequestUncertain(RuntimeError): """For a create request, retain the exact original key and parameters.""" class _NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None # Never forward a Bearer key to a redirected address. class MailDock: def __init__(self, api_key=None, base_url=None, timeout=30): self.api_key = api_key or os.environ.get('MAILDOCK_API_KEY', '') self.base_url = (base_url or os.environ.get('MAILDOCK_BASE_URL') or DEFAULT_BASE).rstrip('/') parts = urlsplit(self.base_url) local_http = parts.scheme == 'http' and parts.hostname in ('localhost', '127.0.0.1', '::1') if (parts.scheme != 'https' and not local_http) or not parts.hostname or parts.username or parts.password or parts.query or parts.fragment or parts.path: raise ValueError('base_url must be an HTTPS origin (HTTP allowed only on loopback).') if not self.api_key: raise ValueError('Set MAILDOCK_API_KEY in your environment.') if timeout <= 0: raise ValueError('timeout must be positive.') self.timeout = timeout self._opener = build_opener(_NoRedirect()) def _request(self, method, path, body=None, idempotency_key=None, timeout=None): headers = {'Authorization': f'Bearer {self.api_key}', 'Accept': 'application/json'} data = None if body is not None: data = json.dumps(body).encode('utf-8') headers['Content-Type'] = 'application/json' if idempotency_key is not None: headers['Idempotency-Key'] = idempotency_key request = Request(self.base_url + path, data=data, headers=headers, method=method) try: with self._opener.open(request, timeout=self.timeout if timeout is None else timeout) as response: status, raw, response_headers = response.status, response.read(), response.headers except HTTPError as exc: status, raw, response_headers = exc.code, exc.read(), exc.headers except (URLError, TimeoutError, OSError) as exc: raise RequestUncertain(f'{method} request failed; preserve the order ID. For create, replay only the exact original key and parameters.') from exc try: payload = json.loads(raw) except (ValueError, UnicodeError): raise RequestUncertain(f'HTTP {status}: non-JSON response; a write may already have committed. Preserve the original request.') from None if not 200 <= status < 300: error = payload.get('error', {}) if isinstance(payload, dict) else {} try: delay = max(1, int(response_headers.get('Retry-After', '60'))) except (ValueError, TypeError): delay = 60 raise APIError(status, error.get('code', 'http_error'), error.get('message', 'Request failed; preserve the original request.'), delay) if not isinstance(payload, dict): raise RequestUncertain('Unexpected response shape; preserve the original request.') return payload @staticmethod def _order_path(order_id): return '/v1/mail-orders/' + str(UUID(str(order_id))) def balance(self): return self._request('GET', '/v1/balance') def products(self, search='', domain='', page=1): return self._request('GET', '/v1/products?' + urlencode({'search': search, 'domain': domain, 'page': page})) def orders(self, page=1): return self._request('GET', '/v1/mail-orders?' + urlencode({'page': page})) def transactions(self, page=1): return self._request('GET', '/v1/transactions?' + urlencode({'page': page})) def create(self, product_id, max_price, idempotency_key): if not isinstance(max_price, str) or not re.fullmatch(r'\d+(\.\d{1,8})?', max_price): raise ValueError('max_price must be a decimal string with at most 8 decimal places.') if not isinstance(idempotency_key, str) or not re.fullmatch(r'[\x21-\x7e]{8,100}', idempotency_key): raise ValueError('Supply a previously persisted key of 8–100 printable ASCII characters without spaces.') return self._request('POST', '/v1/mail-orders', {'product_id': str(UUID(str(product_id))), 'max_price': max_price}, idempotency_key) def order(self, order_id): return self._request('GET', self._order_path(order_id)) def cancel(self, order_id): return self._request('POST', self._order_path(order_id) + '/cancel') def wait_for_code(self, order_id, timeout=600, interval=5): """Use after email allocation and triggering the target verification email. Returns the full Order, including code as a string. Only rate-limited GETs are retried; other errors propagate. Timeout does not cancel the order. """ if timeout <= 0 or interval < 5: raise ValueError('timeout must be positive; interval must be at least 5 seconds.') deadline = time.monotonic() + timeout path = self._order_path(order_id) while time.monotonic() < deadline: delay = interval try: order = self._request('GET', path, timeout=min(self.timeout, max(.001, deadline - time.monotonic())))['order'] if order.get('code') is not None: return order # A review order can already contain a delivered code. if order['state'] in ('review', 'failed', 'canceled', 'completed'): raise RuntimeError(f"Order {order_id}: {order['state']} without code; inspect this order, do not automatically buy again.") except APIError as exc: if exc.status != 429 or exc.code != 'rate_limited': raise delay = max(interval, exc.retry_after) remaining = deadline - time.monotonic() if remaining > 0: time.sleep(min(delay, remaining)) raise TimeoutError(f'Order {order_id}: local wait expired; order was not canceled. Keep the ID and query or cancel explicitly.') def main(): parser = argparse.ArgumentParser(description=__doc__) commands = parser.add_subparsers(dest='command') commands.add_parser('balance') for name in ('products', 'orders', 'transactions'): sub = commands.add_parser(name) sub.add_argument('--page', type=int, default=1) if name == 'products': sub.add_argument('--search', default='') sub.add_argument('--domain', default='') sub = commands.add_parser('create') sub.add_argument('--product-id', required=True) sub.add_argument('--max-price', required=True) sub.add_argument('--idempotency-key', required=True) for name in ('order', 'cancel', 'wait'): sub = commands.add_parser(name) sub.add_argument('order_id') if name == 'wait': sub.add_argument('--timeout', type=float, default=600) args = vars(parser.parse_args()) command = args.pop('command') or 'balance' try: client = MailDock() result = getattr(client, 'wait_for_code' if command == 'wait' else command)(**args) print(json.dumps(result, ensure_ascii=False, indent=2)) except (RuntimeError, TimeoutError, ValueError) as exc: print(str(exc), file=sys.stderr) return 1 return 0 if __name__ == '__main__': sys.exit(main())