# -*- coding: utf-8 -*- """ plane.so FULL COMBO - autoreg + workspace + API token gateway, end-to-end automated ==================================================================================== One account in ~20s, zero captcha. Pipeline per account: 1. POST /auth/email-check/ -> confirm email free (MAGIC_CODE flow) 2. POST /auth/magic-generate/ -> 6-digit code sent to mailbox 3. IMAP poll -> read code from SUBJECT "Plane login code is NNNNNN" 4. GET /auth/get-csrf-token/ -> csrftoken (mandatory, else CSRF failure page) 5. POST /auth/magic-sign-up/ -> 302 + session-id cookie (account created, email auto-verified) 6. GET /api/users/me/ -> verify session 7. POST /api/workspaces/ -> create workspace (name+slug) 8. POST /api/users/api-tokens/ -> issue API token (plane_api_*, 60/min, shown ONCE) 9. GET /api/v1/users/me/ (X-API-Key) -> verify token live 10. append full record to accounts.jsonl Backend: api.plane.so (app.plane.so = SPA only, 405 on /auth). Throttle: auth 10/min per IP -> bulk sleeps 7s. Reverse source: github.com/makeplane/plane (preview branch): apps/api/plane/authentication/urls.py, views/app/magic.py, app/views/api.py CONFIG: fill IMAP_* + EMAIL_* below, or pass --emails file (email:imap_host:imap_user:imap_pass). Usage: python plane_full_combo.py # one account via +alias python plane_full_combo.py --bulk N [out.jsonl] # mass run python plane_full_combo.py --emails list.txt [out.jsonl] Token use afterwards: curl https://api.plane.so/api/v1/users/me/ -H "X-API-Key: [REDACTED]" workspace-scoped: https://api.plane.so/api/v1/workspaces//projects/ etc. """ import imaplib, email as emaillib, email.utils, re, time, json, sys, random, ssl import urllib.request, urllib.error, http.cookiejar from urllib.request import Request, urlopen from urllib.parse import urlencode # ============ CONFIG - FILL YOUR OWN ============ IMAP_HOST = "" # imap.gmail.com / outlook.office365.com / ... IMAP_USER = "" IMAP_PASS = "" # app password recommended EMAIL_DOMAIN = "" # gmail.com EMAIL_LOCAL = "" # yourname -> yourname+prNNNNNN@gmail.com WORKSPACE_NAME = "farm lab" WORKSPACE_SLUG_PREFIX = "farmlab" # ================================================ API = "https://api.plane.so" UA = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Origin": "https://app.plane.so", "Referer": "https://app.plane.so/sign-up/"} def post_json(path, payload, cookie=None, csrf=None, extra=None): hdrs = {"Content-Type": "application/json", **UA} if cookie: hdrs["Cookie"] = cookie if csrf: hdrs["X-CSRFToken"] = csrf if extra: hdrs.update(extra) req = Request(API + path, data=json.dumps(payload).encode(), headers=hdrs) try: with urlopen(req, timeout=30) as r: return r.status, json.loads(r.read().decode() or "{}") except urllib.error.HTTPError as e: try: body = json.loads(e.read().decode() or "{}") except Exception: body = {} return e.code, body def get_csrf(): cj = http.cookiejar.CookieJar() opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) req = urllib.request.Request(API + "/auth/get-csrf-token/", headers=UA) with opener.open(req, timeout=30) as r: r.read() token = [REDACTED] for c in cj if c.name == "csrftoken"), None) return token, "; ".join(f"{c.name}={c.value}" for c in cj) def post_form(path, data, cookies=None, csrf=None): class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, *a, **k): return None opener = urllib.request.build_opener(NoRedirect) hdrs = {"Content-Type": "application/x-www-form-urlencoded", **UA} if cookies: hdrs["Cookie"] = cookies if csrf: hdrs["X-CSRFToken"] = csrf req = urllib.request.Request(API + path, data=urlencode(data).encode(), headers=hdrs) try: r = opener.open(req, timeout=30) return r.status, r.read().decode(errors="ignore"), r.headers except urllib.error.HTTPError as e: return e.code, e.read().decode(errors="ignore"), e.headers def get_json(path, cookie=None, api_key=None): hdrs = {"User-Agent": UA["User-Agent"]} if cookie: hdrs["Cookie"] = cookie if api_key: hdrs["X-API-Key"] = api_key req = Request(API + path, headers=hdrs) try: with urlopen(req, timeout=30) as r: return r.status, json.loads(r.read().decode() or "{}") except urllib.error.HTTPError as e: return e.code, {} def read_otp(match_suffix, host, user, passwd, tries=24, wait=5, since_ts=None): for _ in range(tries): try: m = imaplib.IMAP4_SSL(host, ssl_context=ssl.create_default_context()) m.login(user, passwd); m.select("INBOX") _, data = m.search(None, 'SUBJECT "Plane login code"') best = None for num in data[0].split()[-10:]: _, msg = m.fetch(num, "(RFC822)") raw = msg[0][1] try: mime = emaillib.message_from_bytes(raw) full = "" for part in mime.walk(): if part.get_content_type() in ("text/plain", "text/html"): try: full += part.get_payload(decode=True).decode(errors="ignore") except Exception: pass full += " " + str(mime.get("Subject", "")) + " " + str(mime.get("To", "")) except Exception: full = raw.decode(errors="ignore") if match_suffix.lower() not in full.lower(): continue mm = re.search(r"login code is (\d{6})", full) dm = re.search(r"^Date: (.+)$", raw.decode(errors="ignore"), re.M) ts = 0 if dm: try: ts = email.utils.mktime_tz(email.utils.parsedate_tz(dm.group(1))) except Exception: ts = 0 if mm and (best is None or ts >= best[0]): best = (ts, mm.group(1)) m.logout() if best and (since_ts is None or best[0] >= since_ts - 180): return best[1] except Exception as e: print("imap err:", e, file=sys.stderr) time.sleep(wait) return None def full_combo(email_addr, match_suffix, imap_host, imap_user, imap_pass, slug=None): rec = {"email": email_addr, "ts": int(time.time())} # 1-2. email check + magic generate st, js = post_json("/auth/email-check/", {"email": email_addr}) if js.get("existing"): rec["result"] = "EXISTS"; return rec t0 = time.time() st, js = post_json("/auth/magic-generate/", {"email": email_addr}) if st != 200: rec["result"] = f"GEN_FAIL_{st}"; return rec # 3. OTP code = read_otp(match_suffix, imap_host, imap_user, imap_pass, since_ts=t0) if not code: rec["result"] = "NO_OTP"; return rec # 4-5. csrf + sign-up csrf_tok, csrf_cookie = get_csrf() st, body, hdrs = post_form("/auth/magic-sign-up/", {"email": email_addr, "code": code}, cookies=csrf_cookie, csrf=csrf_tok) loc = hdrs.get("Location", "") if "error_code" in loc: rec["result"] = "SIGNUP_ERR:" + loc[:150]; return rec cookies = hdrs.get_all("Set-Cookie") or [] sess = "; ".join(c.split(";")[0] for c in cookies) cookie = ((sess + "; ") if sess else "") + csrf_cookie csrf_val = next((c.split(";")[0].split("=", 1)[1] for c in cookies if c.startswith("csrftoken=")), csrf_tok) # 6. verify session st, me = get_json("/api/users/me/", cookie=cookie) if st != 200 or not me.get("email"): rec["result"] = f"SESSION_FAIL_{st}"; return rec rec["user_id"] = me["id"] # 7. create workspace slug = slug or (WORKSPACE_SLUG_PREFIX + str(random.randint(10000, 99999))) st, ws = post_json("/api/workspaces/", {"name": WORKSPACE_NAME, "slug": slug}, cookie=cookie, csrf=csrf_val, extra={"Referer": "https://app.plane.so/"}) if st not in (200, 201): rec["result"] = f"WS_FAIL_{st}:{str(ws)[:100]}"; rec["session_cookie"] = cookie; return rec rec["workspace"] = {"slug": ws.get("slug"), "id": ws.get("id")} # 8. API token st, tk = post_json("/api/users/api-tokens/", {"label": "combo-" + slug, "description": "farm"}, cookie=cookie, csrf=csrf_val, extra={"Referer": "https://app.plane.so/"}) if st not in (200, 201) or not tk.get("token"): rec["result"] = f"TOKEN_FAIL_{st}:{str(tk)[:100]}"; rec["session_cookie"] = cookie; return rec rec["api_token"] = tk["token"] rec["rate_limit"] = tk.get("allowed_rate_limit") # 9. verify token on public v1 st, v1 = get_json("/api/v1/users/me/", api_key=tk["token"]) rec["token_live"] = (st == 200) rec["session_cookie"] = cookie rec["result"] = "FULL_OK" if st == 200 else "TOKEN_UNVERIFIED" return rec def gen_alias_email(): alias = "pr" + str(random.randint(100000, 999999)) return f"{EMAIL_LOCAL}+{alias}@{EMAIL_DOMAIN}", alias def run_one(target, imap_host=None, imap_user=None, imap_pass=None): h = imap_host or IMAP_HOST; u = imap_user or IMAP_USER; p = imap_pass or IMAP_PASS if "@" in target: addr, suffix = target, target else: if not (EMAIL_LOCAL and EMAIL_DOMAIN): print("ERROR: fill EMAIL_LOCAL/EMAIL_DOMAIN or pass full email"); sys.exit(1) addr = f"{EMAIL_LOCAL}+{target}@{EMAIL_DOMAIN}"; suffix = target rec = full_combo(addr, suffix, h, u, p) print(json.dumps(rec, indent=2, default=str)) return rec def main(): args = sys.argv[1:] if not args: print(__doc__); sys.exit(1) if args[0] == "--bulk": n = int(args[1]) if len(args) > 1 else 10 outfile = args[2] if len(args) > 2 else "plane_accounts.jsonl" ok = 0 with open(outfile, "a", encoding="utf-8") as f: for i in range(n): addr, alias = gen_alias_email() try: rec = full_combo(addr, alias, IMAP_HOST, IMAP_USER, IMAP_PASS) except Exception as e: rec = {"email": addr, "result": "EXC:" + str(e)} f.write(json.dumps(rec, default=str) + "\n"); f.flush() if rec.get("result") == "FULL_OK": ok += 1 print(f"[{i+1}/{n}] {addr} -> {rec.get('result')} token={rec.get('api_token','-')[:20]} (full_ok={ok})") time.sleep(7) print(f"DONE {ok}/{n} -> {outfile}") elif args[0] == "--emails": outfile = args[2] if len(args) > 2 else "plane_accounts.jsonl" ok = 0 with open(args[1], encoding="utf-8") as f, open(outfile, "a", encoding="utf-8") as o: for i, line in enumerate(f): line = line.strip() if not line: continue parts = line.split(":") if len(parts) == 4: addr, h, u, p = parts else: addr, h, u, p = line, IMAP_HOST, IMAP_USER, IMAP_PASS try: rec = full_combo(addr, addr, h, u, p) except Exception as e: rec = {"email": addr, "result": "EXC:" + str(e)} o.write(json.dumps(rec, default=str) + "\n"); o.flush() if rec.get("result") == "FULL_OK": ok += 1 print(f"[{i+1}] {addr} -> {rec.get('result')} (full_ok={ok})") time.sleep(7) print(f"DONE {ok} -> {outfile}") else: run_one(args[0]) if __name__ == "__main__": main()